{
  "checks": [
    {
      "slug": "changelog",
      "appWidth": 1440,
      "javaScript": true,
      "sourceSha256": "77ff080efe080fc40813e626c7e8d217bd9d5e31d25b7fdeec8e539ef9bda124",
      "before": 2155,
      "innerWidth": 1102,
      "scrollWidth": 1102,
      "height": 2524,
      "proseBelow": [],
      "codeBelow": [],
      "roles": 0,
      "tabindexes": 0,
      "duplicateIds": [],
      "lists": [
        {
          "tag": "OL",
          "start": null,
          "count": 2
        },
        {
          "tag": "UL",
          "start": null,
          "count": 5
        },
        {
          "tag": "UL",
          "start": null,
          "count": 2
        },
        {
          "tag": "UL",
          "start": null,
          "count": 6
        }
      ],
      "regions": [],
      "allDetailsOpen": true,
      "footer": {
        "top": 582.453125,
        "bottom": 632.046875,
        "innerHeight": 680
      },
      "ariaTree": "- main:\n  - paragraph: Changelog\n  - heading \"Kestrel Platform\" [level=1]\n  - paragraph: Every user-visible change to the Kestrel API, CLI and dashboard. Dates are the day a release reached the stable channel.\n  - term: Latest\n  - definition: 4.3.0\n  - term: Released\n  - definition: 4 Sep 2026\n  - term: Channel\n  - definition: stable\n  - term: API version\n  - definition: 2026-06-01\n  - region \"! Breaking Two changes need action before you upgrade\":\n    - heading \"! Breaking Two changes need action before you upgrade\" [level=2]\n    - list:\n      - listitem:\n        - code: GET /v1/runs\n        - text: no longer returns\n        - code: steps[]\n        - text: inline. Fetch them from\n        - code: /v1/runs/:id/steps\n        - text: \", or pass\"\n        - code: \"?expand=steps\"\n        - text: for the old shape. The inline field is removed for good in 5.0.\n      - listitem:\n        - text: The CLI reads\n        - code: KESTREL_TOKEN\n        - text: \", not\"\n        - code: KESTREL_API_KEY\n        - text: . The old variable still works until 1 Dec 2026 and prints a deprecation warning on every invocation.\n        - code: kestrel doctor\n        - text: tells you which one you are on.\n  - region \"4.3.0\":\n    - heading \"4.3.0\" [level=2]\n    - paragraph: 4 Sep 2026\n    - paragraph: Current\n    - paragraph: Scheduled runs get a real backfill story, the run log stops truncating at 10k lines, and the dashboard finally remembers which workspace you were in.\n    - list:\n      - listitem:\n        - text: + Added\n        - heading \"Backfill for scheduled runs\" [level=3]\n        - paragraph:\n          - code: kestrel schedule backfill <id> --from 2026-08-01\n          - text: replays a schedule over a past window, at most 50 runs at a time, with the same concurrency limit as live execution.\n        - paragraph: \"#2841 · docs/schedules#backfill\"\n      - listitem:\n        - text: + Added\n        - heading \"Run log streaming over SSE\" [level=3]\n        - paragraph:\n          - code: GET /v1/runs/:id/logs?stream=true\n          - text: emits log lines as they are written. Logs are retained for 30 days on Team plans, 7 on Free.\n        - paragraph: \"#2803\"\n      - listitem:\n        - text: ↔ Changed\n        - heading \"Run logs keep the last 200k lines, not 10k\" [level=3]\n        - paragraph: Long-running jobs were silently losing their tail, which is precisely the part anyone reads. Older lines are now dropped from the head instead.\n        - paragraph: \"#2790 · reported by 14 workspaces\"\n      - listitem:\n        - text: ✓ Fixed\n        - heading \"Retries no longer double-count against the rate limit\" [level=3]\n        - paragraph: An automatic retry consumed two units of quota instead of one. Affected workspaces have had the difference credited back to the current billing period.\n        - paragraph: \"#2822 · severity: high\"\n      - listitem:\n        - text: − Removed\n        - heading \"The legacy /v0 endpoints are gone\" [level=3]:\n          - text: The legacy\n          - code: /v0\n          - text: endpoints are gone\n        - paragraph: Deprecated since March, at 0.02% of traffic for the last eight weeks. Every caller was contacted directly in August.\n        - paragraph: \"#2755 · announced 12 Mar 2026\"\n  - region \"4.2.2\":\n    - heading \"4.2.2\" [level=2]\n    - paragraph: 21 Aug 2026\n    - paragraph: Patch\n    - paragraph: \"A patch release: one regression from 4.2.0 and two small corrections.\"\n    - list:\n      - listitem:\n        - text: ✓ Fixed\n        - heading \"Webhook signatures verified against the wrong secret after rotation\" [level=3]\n        - paragraph: For roughly six hours after a secret was rotated, deliveries were signed with the retired key. Rotation now writes both keys and accepts either for 24 hours.\n        - paragraph: \"#2731 · postmortem published\"\n      - listitem:\n        - text: ↔ Changed\n        - heading \"Timeouts are reported in seconds everywhere\" [level=3]\n        - paragraph: The API returned milliseconds, the CLI printed seconds, and the dashboard mixed both.\n        - paragraph: \"#2724\"\n  - group: Six earlier releases, 4.0.0 – 4.2.1\n  - paragraph: Kestrel Platform · changelog maintained by the release team\n  - paragraph: Generated 4 Sep 2026, 09:12 UTC",
      "pass": true
    },
    {
      "slug": "changelog",
      "appWidth": 768,
      "javaScript": true,
      "sourceSha256": "77ff080efe080fc40813e626c7e8d217bd9d5e31d25b7fdeec8e539ef9bda124",
      "before": 2648,
      "innerWidth": 718,
      "scrollWidth": 718,
      "height": 3044,
      "proseBelow": [],
      "codeBelow": [],
      "roles": 0,
      "tabindexes": 0,
      "duplicateIds": [],
      "lists": [
        {
          "tag": "OL",
          "start": null,
          "count": 2
        },
        {
          "tag": "UL",
          "start": null,
          "count": 5
        },
        {
          "tag": "UL",
          "start": null,
          "count": 2
        },
        {
          "tag": "UL",
          "start": null,
          "count": 6
        }
      ],
      "regions": [],
      "allDetailsOpen": true,
      "footer": {
        "top": 553.15625,
        "bottom": 640.34375,
        "innerHeight": 680
      },
      "ariaTree": "- main:\n  - paragraph: Changelog\n  - heading \"Kestrel Platform\" [level=1]\n  - paragraph: Every user-visible change to the Kestrel API, CLI and dashboard. Dates are the day a release reached the stable channel.\n  - term: Latest\n  - definition: 4.3.0\n  - term: Released\n  - definition: 4 Sep 2026\n  - term: Channel\n  - definition: stable\n  - term: API version\n  - definition: 2026-06-01\n  - region \"! Breaking Two changes need action before you upgrade\":\n    - heading \"! Breaking Two changes need action before you upgrade\" [level=2]\n    - list:\n      - listitem:\n        - code: GET /v1/runs\n        - text: no longer returns\n        - code: steps[]\n        - text: inline. Fetch them from\n        - code: /v1/runs/:id/steps\n        - text: \", or pass\"\n        - code: \"?expand=steps\"\n        - text: for the old shape. The inline field is removed for good in 5.0.\n      - listitem:\n        - text: The CLI reads\n        - code: KESTREL_TOKEN\n        - text: \", not\"\n        - code: KESTREL_API_KEY\n        - text: . The old variable still works until 1 Dec 2026 and prints a deprecation warning on every invocation.\n        - code: kestrel doctor\n        - text: tells you which one you are on.\n  - region \"4.3.0\":\n    - heading \"4.3.0\" [level=2]\n    - paragraph: 4 Sep 2026\n    - paragraph: Current\n    - paragraph: Scheduled runs get a real backfill story, the run log stops truncating at 10k lines, and the dashboard finally remembers which workspace you were in.\n    - list:\n      - listitem:\n        - text: + Added\n        - heading \"Backfill for scheduled runs\" [level=3]\n        - paragraph:\n          - code: kestrel schedule backfill <id> --from 2026-08-01\n          - text: replays a schedule over a past window, at most 50 runs at a time, with the same concurrency limit as live execution.\n        - paragraph: \"#2841 · docs/schedules#backfill\"\n      - listitem:\n        - text: + Added\n        - heading \"Run log streaming over SSE\" [level=3]\n        - paragraph:\n          - code: GET /v1/runs/:id/logs?stream=true\n          - text: emits log lines as they are written. Logs are retained for 30 days on Team plans, 7 on Free.\n        - paragraph: \"#2803\"\n      - listitem:\n        - text: ↔ Changed\n        - heading \"Run logs keep the last 200k lines, not 10k\" [level=3]\n        - paragraph: Long-running jobs were silently losing their tail, which is precisely the part anyone reads. Older lines are now dropped from the head instead.\n        - paragraph: \"#2790 · reported by 14 workspaces\"\n      - listitem:\n        - text: ✓ Fixed\n        - heading \"Retries no longer double-count against the rate limit\" [level=3]\n        - paragraph: An automatic retry consumed two units of quota instead of one. Affected workspaces have had the difference credited back to the current billing period.\n        - paragraph: \"#2822 · severity: high\"\n      - listitem:\n        - text: − Removed\n        - heading \"The legacy /v0 endpoints are gone\" [level=3]:\n          - text: The legacy\n          - code: /v0\n          - text: endpoints are gone\n        - paragraph: Deprecated since March, at 0.02% of traffic for the last eight weeks. Every caller was contacted directly in August.\n        - paragraph: \"#2755 · announced 12 Mar 2026\"\n  - region \"4.2.2\":\n    - heading \"4.2.2\" [level=2]\n    - paragraph: 21 Aug 2026\n    - paragraph: Patch\n    - paragraph: \"A patch release: one regression from 4.2.0 and two small corrections.\"\n    - list:\n      - listitem:\n        - text: ✓ Fixed\n        - heading \"Webhook signatures verified against the wrong secret after rotation\" [level=3]\n        - paragraph: For roughly six hours after a secret was rotated, deliveries were signed with the retired key. Rotation now writes both keys and accepts either for 24 hours.\n        - paragraph: \"#2731 · postmortem published\"\n      - listitem:\n        - text: ↔ Changed\n        - heading \"Timeouts are reported in seconds everywhere\" [level=3]\n        - paragraph: The API returned milliseconds, the CLI printed seconds, and the dashboard mixed both.\n        - paragraph: \"#2724\"\n  - group: Six earlier releases, 4.0.0 – 4.2.1\n  - paragraph: Kestrel Platform · changelog maintained by the release team\n  - paragraph: Generated 4 Sep 2026, 09:12 UTC",
      "pass": true
    },
    {
      "slug": "changelog",
      "appWidth": 390,
      "javaScript": true,
      "sourceSha256": "77ff080efe080fc40813e626c7e8d217bd9d5e31d25b7fdeec8e539ef9bda124",
      "before": 3451,
      "innerWidth": 356,
      "scrollWidth": 356,
      "height": 4119,
      "proseBelow": [],
      "codeBelow": [],
      "roles": 0,
      "tabindexes": 0,
      "duplicateIds": [],
      "lists": [
        {
          "tag": "OL",
          "start": null,
          "count": 2
        },
        {
          "tag": "UL",
          "start": null,
          "count": 5
        },
        {
          "tag": "UL",
          "start": null,
          "count": 2
        },
        {
          "tag": "UL",
          "start": null,
          "count": 6
        }
      ],
      "regions": [],
      "allDetailsOpen": true,
      "footer": {
        "top": 331.453125,
        "bottom": 444.234375,
        "innerHeight": 480
      },
      "ariaTree": "- main:\n  - paragraph: Changelog\n  - heading \"Kestrel Platform\" [level=1]\n  - paragraph: Every user-visible change to the Kestrel API, CLI and dashboard. Dates are the day a release reached the stable channel.\n  - term: Latest\n  - definition: 4.3.0\n  - term: Released\n  - definition: 4 Sep 2026\n  - term: Channel\n  - definition: stable\n  - term: API version\n  - definition: 2026-06-01\n  - region \"! Breaking Two changes need action before you upgrade\":\n    - heading \"! Breaking Two changes need action before you upgrade\" [level=2]\n    - list:\n      - listitem:\n        - code: GET /v1/runs\n        - text: no longer returns\n        - code: steps[]\n        - text: inline. Fetch them from\n        - code: /v1/runs/:id/steps\n        - text: \", or pass\"\n        - code: \"?expand=steps\"\n        - text: for the old shape. The inline field is removed for good in 5.0.\n      - listitem:\n        - text: The CLI reads\n        - code: KESTREL_TOKEN\n        - text: \", not\"\n        - code: KESTREL_API_KEY\n        - text: . The old variable still works until 1 Dec 2026 and prints a deprecation warning on every invocation.\n        - code: kestrel doctor\n        - text: tells you which one you are on.\n  - region \"4.3.0\":\n    - heading \"4.3.0\" [level=2]\n    - paragraph: 4 Sep 2026\n    - paragraph: Current\n    - paragraph: Scheduled runs get a real backfill story, the run log stops truncating at 10k lines, and the dashboard finally remembers which workspace you were in.\n    - list:\n      - listitem:\n        - text: + Added\n        - heading \"Backfill for scheduled runs\" [level=3]\n        - paragraph:\n          - code: kestrel schedule backfill <id> --from 2026-08-01\n          - text: replays a schedule over a past window, at most 50 runs at a time, with the same concurrency limit as live execution.\n        - paragraph: \"#2841 · docs/schedules#backfill\"\n      - listitem:\n        - text: + Added\n        - heading \"Run log streaming over SSE\" [level=3]\n        - paragraph:\n          - code: GET /v1/runs/:id/logs?stream=true\n          - text: emits log lines as they are written. Logs are retained for 30 days on Team plans, 7 on Free.\n        - paragraph: \"#2803\"\n      - listitem:\n        - text: ↔ Changed\n        - heading \"Run logs keep the last 200k lines, not 10k\" [level=3]\n        - paragraph: Long-running jobs were silently losing their tail, which is precisely the part anyone reads. Older lines are now dropped from the head instead.\n        - paragraph: \"#2790 · reported by 14 workspaces\"\n      - listitem:\n        - text: ✓ Fixed\n        - heading \"Retries no longer double-count against the rate limit\" [level=3]\n        - paragraph: An automatic retry consumed two units of quota instead of one. Affected workspaces have had the difference credited back to the current billing period.\n        - paragraph: \"#2822 · severity: high\"\n      - listitem:\n        - text: − Removed\n        - heading \"The legacy /v0 endpoints are gone\" [level=3]:\n          - text: The legacy\n          - code: /v0\n          - text: endpoints are gone\n        - paragraph: Deprecated since March, at 0.02% of traffic for the last eight weeks. Every caller was contacted directly in August.\n        - paragraph: \"#2755 · announced 12 Mar 2026\"\n  - region \"4.2.2\":\n    - heading \"4.2.2\" [level=2]\n    - paragraph: 21 Aug 2026\n    - paragraph: Patch\n    - paragraph: \"A patch release: one regression from 4.2.0 and two small corrections.\"\n    - list:\n      - listitem:\n        - text: ✓ Fixed\n        - heading \"Webhook signatures verified against the wrong secret after rotation\" [level=3]\n        - paragraph: For roughly six hours after a secret was rotated, deliveries were signed with the retired key. Rotation now writes both keys and accepts either for 24 hours.\n        - paragraph: \"#2731 · postmortem published\"\n      - listitem:\n        - text: ↔ Changed\n        - heading \"Timeouts are reported in seconds everywhere\" [level=3]\n        - paragraph: The API returned milliseconds, the CLI printed seconds, and the dashboard mixed both.\n        - paragraph: \"#2724\"\n  - group: Six earlier releases, 4.0.0 – 4.2.1\n  - paragraph: Kestrel Platform · changelog maintained by the release team\n  - paragraph: Generated 4 Sep 2026, 09:12 UTC",
      "pass": true,
      "footerScreenshot": "changelog--sandbox-footer-390.png",
      "footerScreenshotSha256": "f6b00ad1c1e07c679e21f6b535f44e5e63527f9e36be292a0a0eb6c922a96a01"
    },
    {
      "slug": "changelog",
      "appWidth": 320,
      "javaScript": true,
      "sourceSha256": "77ff080efe080fc40813e626c7e8d217bd9d5e31d25b7fdeec8e539ef9bda124",
      "before": 4098,
      "innerWidth": 286,
      "scrollWidth": 286,
      "height": 4902,
      "proseBelow": [],
      "codeBelow": [],
      "roles": 0,
      "tabindexes": 0,
      "duplicateIds": [],
      "lists": [
        {
          "tag": "OL",
          "start": null,
          "count": 2
        },
        {
          "tag": "UL",
          "start": null,
          "count": 5
        },
        {
          "tag": "UL",
          "start": null,
          "count": 2
        },
        {
          "tag": "UL",
          "start": null,
          "count": 6
        }
      ],
      "regions": [],
      "allDetailsOpen": true,
      "footer": {
        "top": 330.890625,
        "bottom": 443.671875,
        "innerHeight": 480
      },
      "ariaTree": "- main:\n  - paragraph: Changelog\n  - heading \"Kestrel Platform\" [level=1]\n  - paragraph: Every user-visible change to the Kestrel API, CLI and dashboard. Dates are the day a release reached the stable channel.\n  - term: Latest\n  - definition: 4.3.0\n  - term: Released\n  - definition: 4 Sep 2026\n  - term: Channel\n  - definition: stable\n  - term: API version\n  - definition: 2026-06-01\n  - region \"! Breaking Two changes need action before you upgrade\":\n    - heading \"! Breaking Two changes need action before you upgrade\" [level=2]\n    - list:\n      - listitem:\n        - code: GET /v1/runs\n        - text: no longer returns\n        - code: steps[]\n        - text: inline. Fetch them from\n        - code: /v1/runs/:id/steps\n        - text: \", or pass\"\n        - code: \"?expand=steps\"\n        - text: for the old shape. The inline field is removed for good in 5.0.\n      - listitem:\n        - text: The CLI reads\n        - code: KESTREL_TOKEN\n        - text: \", not\"\n        - code: KESTREL_API_KEY\n        - text: . The old variable still works until 1 Dec 2026 and prints a deprecation warning on every invocation.\n        - code: kestrel doctor\n        - text: tells you which one you are on.\n  - region \"4.3.0\":\n    - heading \"4.3.0\" [level=2]\n    - paragraph: 4 Sep 2026\n    - paragraph: Current\n    - paragraph: Scheduled runs get a real backfill story, the run log stops truncating at 10k lines, and the dashboard finally remembers which workspace you were in.\n    - list:\n      - listitem:\n        - text: + Added\n        - heading \"Backfill for scheduled runs\" [level=3]\n        - paragraph:\n          - code: kestrel schedule backfill <id> --from 2026-08-01\n          - text: replays a schedule over a past window, at most 50 runs at a time, with the same concurrency limit as live execution.\n        - paragraph: \"#2841 · docs/schedules#backfill\"\n      - listitem:\n        - text: + Added\n        - heading \"Run log streaming over SSE\" [level=3]\n        - paragraph:\n          - code: GET /v1/runs/:id/logs?stream=true\n          - text: emits log lines as they are written. Logs are retained for 30 days on Team plans, 7 on Free.\n        - paragraph: \"#2803\"\n      - listitem:\n        - text: ↔ Changed\n        - heading \"Run logs keep the last 200k lines, not 10k\" [level=3]\n        - paragraph: Long-running jobs were silently losing their tail, which is precisely the part anyone reads. Older lines are now dropped from the head instead.\n        - paragraph: \"#2790 · reported by 14 workspaces\"\n      - listitem:\n        - text: ✓ Fixed\n        - heading \"Retries no longer double-count against the rate limit\" [level=3]\n        - paragraph: An automatic retry consumed two units of quota instead of one. Affected workspaces have had the difference credited back to the current billing period.\n        - paragraph: \"#2822 · severity: high\"\n      - listitem:\n        - text: − Removed\n        - heading \"The legacy /v0 endpoints are gone\" [level=3]:\n          - text: The legacy\n          - code: /v0\n          - text: endpoints are gone\n        - paragraph: Deprecated since March, at 0.02% of traffic for the last eight weeks. Every caller was contacted directly in August.\n        - paragraph: \"#2755 · announced 12 Mar 2026\"\n  - region \"4.2.2\":\n    - heading \"4.2.2\" [level=2]\n    - paragraph: 21 Aug 2026\n    - paragraph: Patch\n    - paragraph: \"A patch release: one regression from 4.2.0 and two small corrections.\"\n    - list:\n      - listitem:\n        - text: ✓ Fixed\n        - heading \"Webhook signatures verified against the wrong secret after rotation\" [level=3]\n        - paragraph: For roughly six hours after a secret was rotated, deliveries were signed with the retired key. Rotation now writes both keys and accepts either for 24 hours.\n        - paragraph: \"#2731 · postmortem published\"\n      - listitem:\n        - text: ↔ Changed\n        - heading \"Timeouts are reported in seconds everywhere\" [level=3]\n        - paragraph: The API returned milliseconds, the CLI printed seconds, and the dashboard mixed both.\n        - paragraph: \"#2724\"\n  - group: Six earlier releases, 4.0.0 – 4.2.1\n  - paragraph: Kestrel Platform · changelog maintained by the release team\n  - paragraph: Generated 4 Sep 2026, 09:12 UTC",
      "pass": true
    },
    {
      "slug": "migration-guide",
      "appWidth": 1440,
      "javaScript": true,
      "sourceSha256": "f903fe929606e0538cd66a5ca3f316055a5b17edd808f3b6d2c66bc0431d8606",
      "before": 2463,
      "innerWidth": 1102,
      "scrollWidth": 1102,
      "height": 2593,
      "proseBelow": [],
      "codeBelow": [],
      "roles": 0,
      "tabindexes": 0,
      "duplicateIds": [],
      "lists": [
        {
          "tag": "UL",
          "start": null,
          "count": 3
        },
        {
          "tag": "OL",
          "start": null,
          "count": 3
        },
        {
          "tag": "UL",
          "start": null,
          "count": 3
        }
      ],
      "regions": [
        {
          "ids": "change-1 change-1-label-0",
          "labels": [
            "1 · The client takes an options object",
            "Before · v3"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "change-1 change-1-label-1",
          "labels": [
            "1 · The client takes an options object",
            "After · v4"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "change-2 change-2-label-0",
          "labels": [
            "2 · List calls return a page, not an array",
            "Before · v3"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "change-2 change-2-label-1",
          "labels": [
            "2 · List calls return a page, not an array",
            "After · v4"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "upgrade-1",
          "labels": [
            "Upgrade the dependency"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "upgrade-2",
          "labels": [
            "Run the codemod"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "upgrade-3",
          "labels": [
            "Verify before you deploy"
          ],
          "overflow": false,
          "tag": "SECTION"
        }
      ],
      "allDetailsOpen": true,
      "footer": {
        "top": 569.21875,
        "bottom": 638.21875,
        "innerHeight": 680
      },
      "ariaTree": "- main:\n  - paragraph: Migration guide\n  - heading \"Halyard SDK v3 → v4\" [level=1]\n  - paragraph: 3.8.2 → 4.1.0\n  - paragraph: \"Three breaking changes, one codemod, about twenty minutes. No coordinated deploy: v3 and v4 clients can talk to the API at the same time.\"\n  - text: Deadline\n  - paragraph:\n    - text: v3 stops receiving security fixes on\n    - strong: 1 December 2026\n    - text: — 88 days away.\n  - term: Applies to\n  - definition: JS / TS SDK\n  - term: Effort\n  - definition: ~20 min\n  - term: Breaking\n  - definition: 3 changes\n  - term: Rollback\n  - definition: Safe, any time\n  - heading \"What breaks, and what to write instead\" [level=2]\n  - article:\n    - heading \"1 · The client takes an options object\" [level=3]\n    - paragraph: A bare API-key string is no longer accepted, and the export is now named.\n    - paragraph: − Before · v3\n    - region \"1 · The client takes an options object − Before · v3\":\n      - code: import Halyard from \"halyard\"; const hal = new Halyard( process.env.HALYARD_API_KEY );\n    - paragraph: + After · v4\n    - region \"1 · The client takes an options object + After · v4\":\n      - code: \"import { Halyard } from \\\"halyard\\\"; const hal = new Halyard({ token: process.env.HALYARD_TOKEN, });\"\n  - article:\n    - heading \"2 · List calls return a page, not an array\" [level=3]\n    - paragraph:\n      - text: Every\n      - code: list*\n      - text: method returns\n      - code: \"{ data, cursor }\"\n      - text: . Iterate the client when you want all of it — it pages for you.\n    - paragraph: − Before · v3\n    - region \"2 · List calls return a page, not an array − Before · v3\":\n      - code: const runs = await hal.listRuns(); runs.map(r => r.id); // silently capped at 100\n    - paragraph: + After · v4\n    - region \"2 · List calls return a page, not an array + After · v4\":\n      - code: \"const page = await hal.listRuns(); page.data.map(r => r.id); for await (const r of hal.runs) {}\"\n  - article:\n    - heading \"3 · Three renames, no other behaviour change\" [level=3]\n    - paragraph: The codemod does all three; they are listed so you can grep for them yourself.\n    - paragraph: − v3 / + v4\n    - list:\n      - listitem: − err.code === 429 + err.type === \"rate_limited\"\n      - listitem: − hal.close() + await hal.dispose()\n      - listitem: − HALYARD_API_KEY + HALYARD_TOKEN\n  - heading \"Do it in three steps\" [level=2]\n  - list:\n    - listitem:\n      - heading \"Upgrade the dependency\" [level=3]\n      - region \"Upgrade the dependency\":\n        - code: $ npm install halyard@^4.1.0\n    - listitem:\n      - heading \"Run the codemod\" [level=3]\n      - paragraph:\n        - text: It rewrites constructors, renames and\n        - code: err.code\n        - text: comparisons. Where it is not sure it leaves a\n        - code: TODO(halyard)\n        - text: —\n        - code: grep -rn \"TODO(halyard)\" src/\n        - text: to find them. Pagination is the one only you can decide.\n      - region \"Run the codemod\":\n        - code: $ npx halyard-codemod v3-to-v4 src/ 42 files scanned 17 files changed 3 need a human · see TODO(halyard)\n    - listitem:\n      - heading \"Verify before you deploy\" [level=3]\n      - paragraph: Strict mode turns every remaining v3 shape into a thrown error instead of a warning, so the test run tells you the truth.\n      - region \"Verify before you deploy\":\n        - code: $ HALYARD_STRICT=1 npm test ✓ 184 passing ✓ 0 deprecation warnings\n  - heading \"If it goes wrong\" [level=2]\n  - paragraph:\n    - text: Two errors account for almost every report. Rolling back is reinstalling\n    - code: halyard@3.8.2\n    - text: and reverting the codemod commit.\n  - group: \"TypeError: runs.map is not a function\"\n  - group: \"HalyardError: missing_token\"\n  - heading \"The dates that matter\" [level=2]\n  - list:\n    - listitem: 12 Aug 2026 v4.0.0 released, v3 enters maintenance.\n    - listitem: 1 Dec 2026 v3 security fixes stop. Plan for this one.\n    - listitem: 1 Mar 2027 v3 unpublished, support ends.\n  - paragraph:\n    - text: Halyard SDK · migration guide revised 4 Sep 2026 ·\n    - link \"halyard.dev/docs/v4\":\n      - /url: https://halyard.dev/docs/v4",
      "pass": true
    },
    {
      "slug": "migration-guide",
      "appWidth": 768,
      "javaScript": true,
      "sourceSha256": "f903fe929606e0538cd66a5ca3f316055a5b17edd808f3b6d2c66bc0431d8606",
      "before": 3802,
      "innerWidth": 718,
      "scrollWidth": 718,
      "height": 3960,
      "proseBelow": [],
      "codeBelow": [],
      "roles": 0,
      "tabindexes": 0,
      "duplicateIds": [],
      "lists": [
        {
          "tag": "UL",
          "start": null,
          "count": 3
        },
        {
          "tag": "OL",
          "start": null,
          "count": 3
        },
        {
          "tag": "UL",
          "start": null,
          "count": 3
        }
      ],
      "regions": [
        {
          "ids": "change-1 change-1-label-0",
          "labels": [
            "1 · The client takes an options object",
            "Before · v3"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "change-1 change-1-label-1",
          "labels": [
            "1 · The client takes an options object",
            "After · v4"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "change-2 change-2-label-0",
          "labels": [
            "2 · List calls return a page, not an array",
            "Before · v3"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "change-2 change-2-label-1",
          "labels": [
            "2 · List calls return a page, not an array",
            "After · v4"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "upgrade-1",
          "labels": [
            "Upgrade the dependency"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "upgrade-2",
          "labels": [
            "Run the codemod"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "upgrade-3",
          "labels": [
            "Verify before you deploy"
          ],
          "overflow": false,
          "tag": "SECTION"
        }
      ],
      "allDetailsOpen": true,
      "footer": {
        "top": 574.515625,
        "bottom": 643.515625,
        "innerHeight": 680
      },
      "ariaTree": "- main:\n  - paragraph: Migration guide\n  - heading \"Halyard SDK v3 → v4\" [level=1]\n  - paragraph: 3.8.2 → 4.1.0\n  - paragraph: \"Three breaking changes, one codemod, about twenty minutes. No coordinated deploy: v3 and v4 clients can talk to the API at the same time.\"\n  - text: Deadline\n  - paragraph:\n    - text: v3 stops receiving security fixes on\n    - strong: 1 December 2026\n    - text: — 88 days away.\n  - term: Applies to\n  - definition: JS / TS SDK\n  - term: Effort\n  - definition: ~20 min\n  - term: Breaking\n  - definition: 3 changes\n  - term: Rollback\n  - definition: Safe, any time\n  - heading \"What breaks, and what to write instead\" [level=2]\n  - article:\n    - heading \"1 · The client takes an options object\" [level=3]\n    - paragraph: A bare API-key string is no longer accepted, and the export is now named.\n    - paragraph: − Before · v3\n    - region \"1 · The client takes an options object − Before · v3\":\n      - code: import Halyard from \"halyard\"; const hal = new Halyard( process.env.HALYARD_API_KEY );\n    - paragraph: + After · v4\n    - region \"1 · The client takes an options object + After · v4\":\n      - code: \"import { Halyard } from \\\"halyard\\\"; const hal = new Halyard({ token: process.env.HALYARD_TOKEN, });\"\n  - article:\n    - heading \"2 · List calls return a page, not an array\" [level=3]\n    - paragraph:\n      - text: Every\n      - code: list*\n      - text: method returns\n      - code: \"{ data, cursor }\"\n      - text: . Iterate the client when you want all of it — it pages for you.\n    - paragraph: − Before · v3\n    - region \"2 · List calls return a page, not an array − Before · v3\":\n      - code: const runs = await hal.listRuns(); runs.map(r => r.id); // silently capped at 100\n    - paragraph: + After · v4\n    - region \"2 · List calls return a page, not an array + After · v4\":\n      - code: \"const page = await hal.listRuns(); page.data.map(r => r.id); for await (const r of hal.runs) {}\"\n  - article:\n    - heading \"3 · Three renames, no other behaviour change\" [level=3]\n    - paragraph: The codemod does all three; they are listed so you can grep for them yourself.\n    - paragraph: − v3 / + v4\n    - list:\n      - listitem: − err.code === 429 + err.type === \"rate_limited\"\n      - listitem: − hal.close() + await hal.dispose()\n      - listitem: − HALYARD_API_KEY + HALYARD_TOKEN\n  - heading \"Do it in three steps\" [level=2]\n  - list:\n    - listitem:\n      - heading \"Upgrade the dependency\" [level=3]\n      - region \"Upgrade the dependency\":\n        - code: $ npm install halyard@^4.1.0\n    - listitem:\n      - heading \"Run the codemod\" [level=3]\n      - paragraph:\n        - text: It rewrites constructors, renames and\n        - code: err.code\n        - text: comparisons. Where it is not sure it leaves a\n        - code: TODO(halyard)\n        - text: —\n        - code: grep -rn \"TODO(halyard)\" src/\n        - text: to find them. Pagination is the one only you can decide.\n      - region \"Run the codemod\":\n        - code: $ npx halyard-codemod v3-to-v4 src/ 42 files scanned 17 files changed 3 need a human · see TODO(halyard)\n    - listitem:\n      - heading \"Verify before you deploy\" [level=3]\n      - paragraph: Strict mode turns every remaining v3 shape into a thrown error instead of a warning, so the test run tells you the truth.\n      - region \"Verify before you deploy\":\n        - code: $ HALYARD_STRICT=1 npm test ✓ 184 passing ✓ 0 deprecation warnings\n  - heading \"If it goes wrong\" [level=2]\n  - paragraph:\n    - text: Two errors account for almost every report. Rolling back is reinstalling\n    - code: halyard@3.8.2\n    - text: and reverting the codemod commit.\n  - group: \"TypeError: runs.map is not a function\"\n  - group: \"HalyardError: missing_token\"\n  - heading \"The dates that matter\" [level=2]\n  - list:\n    - listitem: 12 Aug 2026 v4.0.0 released, v3 enters maintenance.\n    - listitem: 1 Dec 2026 v3 security fixes stop. Plan for this one.\n    - listitem: 1 Mar 2027 v3 unpublished, support ends.\n  - paragraph:\n    - text: Halyard SDK · migration guide revised 4 Sep 2026 ·\n    - link \"halyard.dev/docs/v4\":\n      - /url: https://halyard.dev/docs/v4",
      "pass": true
    },
    {
      "slug": "migration-guide",
      "appWidth": 390,
      "javaScript": true,
      "sourceSha256": "f903fe929606e0538cd66a5ca3f316055a5b17edd808f3b6d2c66bc0431d8606",
      "before": 4450,
      "innerWidth": 356,
      "scrollWidth": 356,
      "height": 4690,
      "proseBelow": [],
      "codeBelow": [],
      "roles": 0,
      "tabindexes": 0,
      "duplicateIds": [],
      "lists": [
        {
          "tag": "UL",
          "start": null,
          "count": 3
        },
        {
          "tag": "OL",
          "start": null,
          "count": 3
        },
        {
          "tag": "UL",
          "start": null,
          "count": 3
        }
      ],
      "regions": [
        {
          "ids": "change-1 change-1-label-0",
          "labels": [
            "1 · The client takes an options object",
            "Before · v3"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "change-1 change-1-label-1",
          "labels": [
            "1 · The client takes an options object",
            "After · v4"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "change-2 change-2-label-0",
          "labels": [
            "2 · List calls return a page, not an array",
            "Before · v3"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "change-2 change-2-label-1",
          "labels": [
            "2 · List calls return a page, not an array",
            "After · v4"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "upgrade-1",
          "labels": [
            "Upgrade the dependency"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "upgrade-2",
          "labels": [
            "Run the codemod"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "upgrade-3",
          "labels": [
            "Verify before you deploy"
          ],
          "overflow": false,
          "tag": "SECTION"
        }
      ],
      "allDetailsOpen": true,
      "footer": {
        "top": 349.046875,
        "bottom": 443.640625,
        "innerHeight": 480
      },
      "ariaTree": "- main:\n  - paragraph: Migration guide\n  - heading \"Halyard SDK v3 → v4\" [level=1]\n  - paragraph: 3.8.2 → 4.1.0\n  - paragraph: \"Three breaking changes, one codemod, about twenty minutes. No coordinated deploy: v3 and v4 clients can talk to the API at the same time.\"\n  - text: Deadline\n  - paragraph:\n    - text: v3 stops receiving security fixes on\n    - strong: 1 December 2026\n    - text: — 88 days away.\n  - term: Applies to\n  - definition: JS / TS SDK\n  - term: Effort\n  - definition: ~20 min\n  - term: Breaking\n  - definition: 3 changes\n  - term: Rollback\n  - definition: Safe, any time\n  - heading \"What breaks, and what to write instead\" [level=2]\n  - article:\n    - heading \"1 · The client takes an options object\" [level=3]\n    - paragraph: A bare API-key string is no longer accepted, and the export is now named.\n    - paragraph: − Before · v3\n    - region \"1 · The client takes an options object − Before · v3\":\n      - code: import Halyard from \"halyard\"; const hal = new Halyard( process.env.HALYARD_API_KEY );\n    - paragraph: + After · v4\n    - region \"1 · The client takes an options object + After · v4\":\n      - code: \"import { Halyard } from \\\"halyard\\\"; const hal = new Halyard({ token: process.env.HALYARD_TOKEN, });\"\n  - article:\n    - heading \"2 · List calls return a page, not an array\" [level=3]\n    - paragraph:\n      - text: Every\n      - code: list*\n      - text: method returns\n      - code: \"{ data, cursor }\"\n      - text: . Iterate the client when you want all of it — it pages for you.\n    - paragraph: − Before · v3\n    - region \"2 · List calls return a page, not an array − Before · v3\":\n      - code: const runs = await hal.listRuns(); runs.map(r => r.id); // silently capped at 100\n    - paragraph: + After · v4\n    - region \"2 · List calls return a page, not an array + After · v4\":\n      - code: \"const page = await hal.listRuns(); page.data.map(r => r.id); for await (const r of hal.runs) {}\"\n  - article:\n    - heading \"3 · Three renames, no other behaviour change\" [level=3]\n    - paragraph: The codemod does all three; they are listed so you can grep for them yourself.\n    - paragraph: − v3 / + v4\n    - list:\n      - listitem: − err.code === 429 + err.type === \"rate_limited\"\n      - listitem: − hal.close() + await hal.dispose()\n      - listitem: − HALYARD_API_KEY + HALYARD_TOKEN\n  - heading \"Do it in three steps\" [level=2]\n  - list:\n    - listitem:\n      - heading \"Upgrade the dependency\" [level=3]\n      - region \"Upgrade the dependency\":\n        - code: $ npm install halyard@^4.1.0\n    - listitem:\n      - heading \"Run the codemod\" [level=3]\n      - paragraph:\n        - text: It rewrites constructors, renames and\n        - code: err.code\n        - text: comparisons. Where it is not sure it leaves a\n        - code: TODO(halyard)\n        - text: —\n        - code: grep -rn \"TODO(halyard)\" src/\n        - text: to find them. Pagination is the one only you can decide.\n      - region \"Run the codemod\":\n        - code: $ npx halyard-codemod v3-to-v4 src/ 42 files scanned 17 files changed 3 need a human · see TODO(halyard)\n    - listitem:\n      - heading \"Verify before you deploy\" [level=3]\n      - paragraph: Strict mode turns every remaining v3 shape into a thrown error instead of a warning, so the test run tells you the truth.\n      - region \"Verify before you deploy\":\n        - code: $ HALYARD_STRICT=1 npm test ✓ 184 passing ✓ 0 deprecation warnings\n  - heading \"If it goes wrong\" [level=2]\n  - paragraph:\n    - text: Two errors account for almost every report. Rolling back is reinstalling\n    - code: halyard@3.8.2\n    - text: and reverting the codemod commit.\n  - group: \"TypeError: runs.map is not a function\"\n  - group: \"HalyardError: missing_token\"\n  - heading \"The dates that matter\" [level=2]\n  - list:\n    - listitem: 12 Aug 2026 v4.0.0 released, v3 enters maintenance.\n    - listitem: 1 Dec 2026 v3 security fixes stop. Plan for this one.\n    - listitem: 1 Mar 2027 v3 unpublished, support ends.\n  - paragraph:\n    - text: Halyard SDK · migration guide revised 4 Sep 2026 ·\n    - link \"halyard.dev/docs/v4\":\n      - /url: https://halyard.dev/docs/v4",
      "pass": true,
      "footerScreenshot": "migration-guide--sandbox-footer-390.png",
      "footerScreenshotSha256": "60704a9e05ffa24b56bb6bb49b21d311ccb5968235e41ab02c661efdc37a0de8"
    },
    {
      "slug": "migration-guide",
      "appWidth": 320,
      "javaScript": true,
      "sourceSha256": "f903fe929606e0538cd66a5ca3f316055a5b17edd808f3b6d2c66bc0431d8606",
      "before": 4883,
      "innerWidth": 286,
      "scrollWidth": 286,
      "height": 5177,
      "proseBelow": [],
      "codeBelow": [],
      "roles": 0,
      "tabindexes": 0,
      "duplicateIds": [],
      "lists": [
        {
          "tag": "UL",
          "start": null,
          "count": 3
        },
        {
          "tag": "OL",
          "start": null,
          "count": 3
        },
        {
          "tag": "UL",
          "start": null,
          "count": 3
        }
      ],
      "regions": [
        {
          "ids": "change-1 change-1-label-0",
          "labels": [
            "1 · The client takes an options object",
            "Before · v3"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "change-1 change-1-label-1",
          "labels": [
            "1 · The client takes an options object",
            "After · v4"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "change-2 change-2-label-0",
          "labels": [
            "2 · List calls return a page, not an array",
            "Before · v3"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "change-2 change-2-label-1",
          "labels": [
            "2 · List calls return a page, not an array",
            "After · v4"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "upgrade-1",
          "labels": [
            "Upgrade the dependency"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "upgrade-2",
          "labels": [
            "Run the codemod"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "upgrade-3",
          "labels": [
            "Verify before you deploy"
          ],
          "overflow": true,
          "tag": "SECTION"
        }
      ],
      "allDetailsOpen": true,
      "footer": {
        "top": 324.078125,
        "bottom": 444.265625,
        "innerHeight": 480
      },
      "ariaTree": "- main:\n  - paragraph: Migration guide\n  - heading \"Halyard SDK v3 → v4\" [level=1]\n  - paragraph: 3.8.2 → 4.1.0\n  - paragraph: \"Three breaking changes, one codemod, about twenty minutes. No coordinated deploy: v3 and v4 clients can talk to the API at the same time.\"\n  - text: Deadline\n  - paragraph:\n    - text: v3 stops receiving security fixes on\n    - strong: 1 December 2026\n    - text: — 88 days away.\n  - term: Applies to\n  - definition: JS / TS SDK\n  - term: Effort\n  - definition: ~20 min\n  - term: Breaking\n  - definition: 3 changes\n  - term: Rollback\n  - definition: Safe, any time\n  - heading \"What breaks, and what to write instead\" [level=2]\n  - article:\n    - heading \"1 · The client takes an options object\" [level=3]\n    - paragraph: A bare API-key string is no longer accepted, and the export is now named.\n    - paragraph: − Before · v3\n    - region \"1 · The client takes an options object − Before · v3\":\n      - code: import Halyard from \"halyard\"; const hal = new Halyard( process.env.HALYARD_API_KEY );\n    - paragraph: + After · v4\n    - region \"1 · The client takes an options object + After · v4\":\n      - code: \"import { Halyard } from \\\"halyard\\\"; const hal = new Halyard({ token: process.env.HALYARD_TOKEN, });\"\n  - article:\n    - heading \"2 · List calls return a page, not an array\" [level=3]\n    - paragraph:\n      - text: Every\n      - code: list*\n      - text: method returns\n      - code: \"{ data, cursor }\"\n      - text: . Iterate the client when you want all of it — it pages for you.\n    - paragraph: − Before · v3\n    - region \"2 · List calls return a page, not an array − Before · v3\":\n      - code: const runs = await hal.listRuns(); runs.map(r => r.id); // silently capped at 100\n    - paragraph: + After · v4\n    - region \"2 · List calls return a page, not an array + After · v4\":\n      - code: \"const page = await hal.listRuns(); page.data.map(r => r.id); for await (const r of hal.runs) {}\"\n  - article:\n    - heading \"3 · Three renames, no other behaviour change\" [level=3]\n    - paragraph: The codemod does all three; they are listed so you can grep for them yourself.\n    - paragraph: − v3 / + v4\n    - list:\n      - listitem: − err.code === 429 + err.type === \"rate_limited\"\n      - listitem: − hal.close() + await hal.dispose()\n      - listitem: − HALYARD_API_KEY + HALYARD_TOKEN\n  - heading \"Do it in three steps\" [level=2]\n  - list:\n    - listitem:\n      - heading \"Upgrade the dependency\" [level=3]\n      - region \"Upgrade the dependency\":\n        - code: $ npm install halyard@^4.1.0\n    - listitem:\n      - heading \"Run the codemod\" [level=3]\n      - paragraph:\n        - text: It rewrites constructors, renames and\n        - code: err.code\n        - text: comparisons. Where it is not sure it leaves a\n        - code: TODO(halyard)\n        - text: —\n        - code: grep -rn \"TODO(halyard)\" src/\n        - text: to find them. Pagination is the one only you can decide.\n      - region \"Run the codemod\":\n        - code: $ npx halyard-codemod v3-to-v4 src/ 42 files scanned 17 files changed 3 need a human · see TODO(halyard)\n    - listitem:\n      - heading \"Verify before you deploy\" [level=3]\n      - paragraph: Strict mode turns every remaining v3 shape into a thrown error instead of a warning, so the test run tells you the truth.\n      - region \"Verify before you deploy\":\n        - code: $ HALYARD_STRICT=1 npm test ✓ 184 passing ✓ 0 deprecation warnings\n  - heading \"If it goes wrong\" [level=2]\n  - paragraph:\n    - text: Two errors account for almost every report. Rolling back is reinstalling\n    - code: halyard@3.8.2\n    - text: and reverting the codemod commit.\n  - group: \"TypeError: runs.map is not a function\"\n  - group: \"HalyardError: missing_token\"\n  - heading \"The dates that matter\" [level=2]\n  - list:\n    - listitem: 12 Aug 2026 v4.0.0 released, v3 enters maintenance.\n    - listitem: 1 Dec 2026 v3 security fixes stop. Plan for this one.\n    - listitem: 1 Mar 2027 v3 unpublished, support ends.\n  - paragraph:\n    - text: Halyard SDK · migration guide revised 4 Sep 2026 ·\n    - link \"halyard.dev/docs/v4\":\n      - /url: https://halyard.dev/docs/v4",
      "pass": true
    },
    {
      "slug": "runbook",
      "appWidth": 1440,
      "javaScript": true,
      "sourceSha256": "64f9d4322e821044456be0a4ebd798339a9eb52d1ef16982759eba6995c965c7",
      "before": 3445,
      "innerWidth": 1102,
      "scrollWidth": 1102,
      "height": 3445,
      "proseBelow": [],
      "codeBelow": [],
      "roles": 0,
      "tabindexes": 0,
      "duplicateIds": [],
      "lists": [
        {
          "tag": "UL",
          "start": null,
          "count": 4
        },
        {
          "tag": "OL",
          "start": null,
          "count": 4
        },
        {
          "tag": "OL",
          "start": "5",
          "count": 1
        },
        {
          "tag": "UL",
          "start": null,
          "count": 2
        }
      ],
      "regions": [
        {
          "ids": "precondition-1",
          "labels": [
            "You are on the VPN and reach the primary."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "precondition-2",
          "labels": [
            "The deploy freeze is off."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "precondition-3",
          "labels": [
            "No migration is running."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "precondition-4",
          "labels": [
            "You can write to the secret store."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "command-1",
          "labels": [
            "Announce it"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "command-2",
          "labels": [
            "Generate the new secret"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "command-3",
          "labels": [
            "Create the successor role"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "command-4",
          "labels": [
            "Point the application at it"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "operation-section-1",
          "labels": [
            "Verify — between steps 4 and 5"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "command-6",
          "labels": [
            "Retire the old role"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "command-7",
          "labels": [
            "Rollback — safe at any point before step 5"
          ],
          "overflow": false,
          "tag": "SECTION"
        }
      ],
      "allDetailsOpen": true,
      "footer": {
        "top": 587.8125,
        "bottom": 636.40625,
        "innerHeight": 680
      },
      "ariaTree": "- main:\n  - paragraph: Runbook\n  - heading \"Rotate the production database credentials\" [level=1]\n  - paragraph:\n    - text: Quarterly rotation of\n    - code: api_rw\n    - text: \"on\"\n    - code: prod-pg-1\n    - text: . Zero downtime — but only if step 4 finishes before you touch step 5.\n  - term: When\n  - definition: Quarterly\n  - term: Takes\n  - definition: ~15 min\n  - term: Downtime\n  - definition: None\n  - term: Needs\n  - definition: db-admin\n  - heading \"Before you start\" [level=2]\n  - paragraph: All four. They take a minute together.\n  - list:\n    - listitem:\n      - text: You are on the VPN and reach the primary.\n      - region \"You are on the VPN and reach the primary.\":\n        - code: psql -h prod-pg-1 -c '\\conninfo'\n    - listitem:\n      - text: The deploy freeze is off.\n      - region \"The deploy freeze is off.\":\n        - code: \"./bin/freeze status # => open\"\n    - listitem:\n      - text: No migration is running.\n      - region \"No migration is running.\":\n        - code: \"./bin/migrations running # => 0\"\n    - listitem:\n      - text: You can write to the secret store.\n      - region \"You can write to the secret store.\":\n        - code: vault kv get -field=version \\ secret/prod/db\n  - paragraph: Stop if any is false Rotating during a migration leaves the migrator holding a dead connection. Wait it out.\n  - heading \"The procedure\" [level=2]\n  - paragraph: Two roles exist at once for a few minutes. That overlap is the design — nothing restarts, nothing has to be timed.\n  - list:\n    - listitem:\n      - heading \"Announce it\" [level=3]\n      - paragraph: A rotation nobody expected costs more than the one you announce.\n      - region \"Announce it\": \"Command ./bin/announce \\\"rotating api_rw, ~15 min, no downtime expected\\\" Output posted to #eng-oncall\"\n    - listitem:\n      - heading \"Generate the new secret\" [level=3]\n      - paragraph: Written to a staging slot the application does not read yet.\n      - region \"Generate the new secret\": Command export NEW=$(openssl rand -hex 24) vault kv put secret/prod/db/next \\ password=\"$NEW\" Output Key Value version 8\n    - listitem:\n      - heading \"Create the successor role\" [level=3]\n      - paragraph:\n        - text: It inherits from the\n        - code: api_rw\n        - text: group role, so you never re-grant table by table.\n      - region \"Create the successor role\": Command psql -h prod-pg-1 \\ -f sql/rotate.sql \\ -v pw=\"$NEW\" -v role=api_rw_q3 Output CREATE ROLE GRANT\n    - listitem:\n      - heading \"Point the application at it\" [level=3]\n      - paragraph: This is the step that matters. The sidecar reloads on its own, no restart. Until it returns, the old role is still serving traffic.\n      - region \"Point the application at it\": Command vault kv put secret/prod/db \\ username=api_rw_q3 \\ password=\"$NEW\" ./bin/wait-for-rollout \\ --key secret/prod/db \\ --timeout 120s Output all 14 pods on version 9 (47s)\n  - heading \"Verify — between steps 4 and 5\" [level=2]\n  - paragraph: Zero on the old role for five straight minutes. That is the bar.\n  - region \"Verify — between steps 4 and 5\": Command psql -h prod-pg-1 -c \"select usename, count(*) from pg_stat_activity where datname='app' group by 1\" Output api_rw_q3 | 42 api_rw_q2 | 0\n  - paragraph: \"If the old role still has connections: a pod missed the reload. Restart it.\"\n  - list:\n    - listitem:\n      - heading \"Retire the old role\" [level=3]\n      - paragraph: Only once the verification check has been clean for five minutes.\n      - region \"Retire the old role\": Command psql -h prod-pg-1 \\ -c \"drop role api_rw_q2\" Output DROP ROLE\n      - paragraph:\n        - text: \"If it says objects depend on it: something still owns tables as that role. Stop and escalate — never\"\n        - code: REASSIGN OWNED\n        - text: on production alone.\n  - heading \"If it goes wrong\" [level=2]\n  - heading \"Rollback — safe at any point before step 5\" [level=3]\n  - paragraph: The old secret version and the old role both still exist, so this is a sixty-second undo — not a deploy.\n  - region \"Rollback — safe at any point before step 5\": Command vault kv rollback -version=8 \\ secret/prod/db ./bin/wait-for-rollout \\ --key secret/prod/db Output all 14 pods back on version 8 (39s)\n  - heading \"When to escalate\" [level=2]\n  - list:\n    - listitem:\n      - text: Connections have not drained after 10 minutes. Page\n      - code: /page data-platform\n      - text: and leave both roles in place.\n    - listitem:\n      - code: DROP ROLE\n      - text: fails for any reason. Stop and page. Two live roles is safe; half-rotated is not.\n  - paragraph: rotate-db-credentials · owner data-platform · reviewed 28 Aug 2026 · last run 12 Jun 2026",
      "pass": true
    },
    {
      "slug": "runbook",
      "appWidth": 768,
      "javaScript": true,
      "sourceSha256": "64f9d4322e821044456be0a4ebd798339a9eb52d1ef16982759eba6995c965c7",
      "before": 3891,
      "innerWidth": 718,
      "scrollWidth": 718,
      "height": 3891,
      "proseBelow": [],
      "codeBelow": [],
      "roles": 0,
      "tabindexes": 0,
      "duplicateIds": [],
      "lists": [
        {
          "tag": "UL",
          "start": null,
          "count": 4
        },
        {
          "tag": "OL",
          "start": null,
          "count": 4
        },
        {
          "tag": "OL",
          "start": "5",
          "count": 1
        },
        {
          "tag": "UL",
          "start": null,
          "count": 2
        }
      ],
      "regions": [
        {
          "ids": "precondition-1",
          "labels": [
            "You are on the VPN and reach the primary."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "precondition-2",
          "labels": [
            "The deploy freeze is off."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "precondition-3",
          "labels": [
            "No migration is running."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "precondition-4",
          "labels": [
            "You can write to the secret store."
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "command-1",
          "labels": [
            "Announce it"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "command-2",
          "labels": [
            "Generate the new secret"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "command-3",
          "labels": [
            "Create the successor role"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "command-4",
          "labels": [
            "Point the application at it"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "operation-section-1",
          "labels": [
            "Verify — between steps 4 and 5"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "command-6",
          "labels": [
            "Retire the old role"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "command-7",
          "labels": [
            "Rollback — safe at any point before step 5"
          ],
          "overflow": false,
          "tag": "SECTION"
        }
      ],
      "allDetailsOpen": true,
      "footer": {
        "top": 566.09375,
        "bottom": 640.28125,
        "innerHeight": 680
      },
      "ariaTree": "- main:\n  - paragraph: Runbook\n  - heading \"Rotate the production database credentials\" [level=1]\n  - paragraph:\n    - text: Quarterly rotation of\n    - code: api_rw\n    - text: \"on\"\n    - code: prod-pg-1\n    - text: . Zero downtime — but only if step 4 finishes before you touch step 5.\n  - term: When\n  - definition: Quarterly\n  - term: Takes\n  - definition: ~15 min\n  - term: Downtime\n  - definition: None\n  - term: Needs\n  - definition: db-admin\n  - heading \"Before you start\" [level=2]\n  - paragraph: All four. They take a minute together.\n  - list:\n    - listitem:\n      - text: You are on the VPN and reach the primary.\n      - region \"You are on the VPN and reach the primary.\":\n        - code: psql -h prod-pg-1 -c '\\conninfo'\n    - listitem:\n      - text: The deploy freeze is off.\n      - region \"The deploy freeze is off.\":\n        - code: \"./bin/freeze status # => open\"\n    - listitem:\n      - text: No migration is running.\n      - region \"No migration is running.\":\n        - code: \"./bin/migrations running # => 0\"\n    - listitem:\n      - text: You can write to the secret store.\n      - region \"You can write to the secret store.\":\n        - code: vault kv get -field=version \\ secret/prod/db\n  - paragraph: Stop if any is false Rotating during a migration leaves the migrator holding a dead connection. Wait it out.\n  - heading \"The procedure\" [level=2]\n  - paragraph: Two roles exist at once for a few minutes. That overlap is the design — nothing restarts, nothing has to be timed.\n  - list:\n    - listitem:\n      - heading \"Announce it\" [level=3]\n      - paragraph: A rotation nobody expected costs more than the one you announce.\n      - region \"Announce it\": \"Command ./bin/announce \\\"rotating api_rw, ~15 min, no downtime expected\\\" Output posted to #eng-oncall\"\n    - listitem:\n      - heading \"Generate the new secret\" [level=3]\n      - paragraph: Written to a staging slot the application does not read yet.\n      - region \"Generate the new secret\": Command export NEW=$(openssl rand -hex 24) vault kv put secret/prod/db/next \\ password=\"$NEW\" Output Key Value version 8\n    - listitem:\n      - heading \"Create the successor role\" [level=3]\n      - paragraph:\n        - text: It inherits from the\n        - code: api_rw\n        - text: group role, so you never re-grant table by table.\n      - region \"Create the successor role\": Command psql -h prod-pg-1 \\ -f sql/rotate.sql \\ -v pw=\"$NEW\" -v role=api_rw_q3 Output CREATE ROLE GRANT\n    - listitem:\n      - heading \"Point the application at it\" [level=3]\n      - paragraph: This is the step that matters. The sidecar reloads on its own, no restart. Until it returns, the old role is still serving traffic.\n      - region \"Point the application at it\": Command vault kv put secret/prod/db \\ username=api_rw_q3 \\ password=\"$NEW\" ./bin/wait-for-rollout \\ --key secret/prod/db \\ --timeout 120s Output all 14 pods on version 9 (47s)\n  - heading \"Verify — between steps 4 and 5\" [level=2]\n  - paragraph: Zero on the old role for five straight minutes. That is the bar.\n  - region \"Verify — between steps 4 and 5\": Command psql -h prod-pg-1 -c \"select usename, count(*) from pg_stat_activity where datname='app' group by 1\" Output api_rw_q3 | 42 api_rw_q2 | 0\n  - paragraph: \"If the old role still has connections: a pod missed the reload. Restart it.\"\n  - list:\n    - listitem:\n      - heading \"Retire the old role\" [level=3]\n      - paragraph: Only once the verification check has been clean for five minutes.\n      - region \"Retire the old role\": Command psql -h prod-pg-1 \\ -c \"drop role api_rw_q2\" Output DROP ROLE\n      - paragraph:\n        - text: \"If it says objects depend on it: something still owns tables as that role. Stop and escalate — never\"\n        - code: REASSIGN OWNED\n        - text: on production alone.\n  - heading \"If it goes wrong\" [level=2]\n  - heading \"Rollback — safe at any point before step 5\" [level=3]\n  - paragraph: The old secret version and the old role both still exist, so this is a sixty-second undo — not a deploy.\n  - region \"Rollback — safe at any point before step 5\": Command vault kv rollback -version=8 \\ secret/prod/db ./bin/wait-for-rollout \\ --key secret/prod/db Output all 14 pods back on version 8 (39s)\n  - heading \"When to escalate\" [level=2]\n  - list:\n    - listitem:\n      - text: Connections have not drained after 10 minutes. Page\n      - code: /page data-platform\n      - text: and leave both roles in place.\n    - listitem:\n      - code: DROP ROLE\n      - text: fails for any reason. Stop and page. Two live roles is safe; half-rotated is not.\n  - paragraph: rotate-db-credentials · owner data-platform · reviewed 28 Aug 2026 · last run 12 Jun 2026",
      "pass": true
    },
    {
      "slug": "runbook",
      "appWidth": 390,
      "javaScript": true,
      "sourceSha256": "64f9d4322e821044456be0a4ebd798339a9eb52d1ef16982759eba6995c965c7",
      "before": 5423,
      "innerWidth": 356,
      "scrollWidth": 356,
      "height": 5423,
      "proseBelow": [],
      "codeBelow": [],
      "roles": 0,
      "tabindexes": 0,
      "duplicateIds": [],
      "lists": [
        {
          "tag": "UL",
          "start": null,
          "count": 4
        },
        {
          "tag": "OL",
          "start": null,
          "count": 4
        },
        {
          "tag": "OL",
          "start": "5",
          "count": 1
        },
        {
          "tag": "UL",
          "start": null,
          "count": 2
        }
      ],
      "regions": [
        {
          "ids": "precondition-1",
          "labels": [
            "You are on the VPN and reach the primary."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "precondition-2",
          "labels": [
            "The deploy freeze is off."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "precondition-3",
          "labels": [
            "No migration is running."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "precondition-4",
          "labels": [
            "You can write to the secret store."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "command-1",
          "labels": [
            "Announce it"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "command-2",
          "labels": [
            "Generate the new secret"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "command-3",
          "labels": [
            "Create the successor role"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "command-4",
          "labels": [
            "Point the application at it"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "operation-section-1",
          "labels": [
            "Verify — between steps 4 and 5"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "command-6",
          "labels": [
            "Retire the old role"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "command-7",
          "labels": [
            "Rollback — safe at any point before step 5"
          ],
          "overflow": true,
          "tag": "SECTION"
        }
      ],
      "allDetailsOpen": true,
      "footer": {
        "top": 343.96875,
        "bottom": 443.75,
        "innerHeight": 480
      },
      "ariaTree": "- main:\n  - paragraph: Runbook\n  - heading \"Rotate the production database credentials\" [level=1]\n  - paragraph:\n    - text: Quarterly rotation of\n    - code: api_rw\n    - text: \"on\"\n    - code: prod-pg-1\n    - text: . Zero downtime — but only if step 4 finishes before you touch step 5.\n  - term: When\n  - definition: Quarterly\n  - term: Takes\n  - definition: ~15 min\n  - term: Downtime\n  - definition: None\n  - term: Needs\n  - definition: db-admin\n  - heading \"Before you start\" [level=2]\n  - paragraph: All four. They take a minute together.\n  - list:\n    - listitem:\n      - text: You are on the VPN and reach the primary.\n      - region \"You are on the VPN and reach the primary.\":\n        - code: psql -h prod-pg-1 -c '\\conninfo'\n    - listitem:\n      - text: The deploy freeze is off.\n      - region \"The deploy freeze is off.\":\n        - code: \"./bin/freeze status # => open\"\n    - listitem:\n      - text: No migration is running.\n      - region \"No migration is running.\":\n        - code: \"./bin/migrations running # => 0\"\n    - listitem:\n      - text: You can write to the secret store.\n      - region \"You can write to the secret store.\":\n        - code: vault kv get -field=version \\ secret/prod/db\n  - paragraph: Stop if any is false Rotating during a migration leaves the migrator holding a dead connection. Wait it out.\n  - heading \"The procedure\" [level=2]\n  - paragraph: Two roles exist at once for a few minutes. That overlap is the design — nothing restarts, nothing has to be timed.\n  - list:\n    - listitem:\n      - heading \"Announce it\" [level=3]\n      - paragraph: A rotation nobody expected costs more than the one you announce.\n      - region \"Announce it\": \"Command ./bin/announce \\\"rotating api_rw, ~15 min, no downtime expected\\\" Output posted to #eng-oncall\"\n    - listitem:\n      - heading \"Generate the new secret\" [level=3]\n      - paragraph: Written to a staging slot the application does not read yet.\n      - region \"Generate the new secret\": Command export NEW=$(openssl rand -hex 24) vault kv put secret/prod/db/next \\ password=\"$NEW\" Output Key Value version 8\n    - listitem:\n      - heading \"Create the successor role\" [level=3]\n      - paragraph:\n        - text: It inherits from the\n        - code: api_rw\n        - text: group role, so you never re-grant table by table.\n      - region \"Create the successor role\": Command psql -h prod-pg-1 \\ -f sql/rotate.sql \\ -v pw=\"$NEW\" -v role=api_rw_q3 Output CREATE ROLE GRANT\n    - listitem:\n      - heading \"Point the application at it\" [level=3]\n      - paragraph: This is the step that matters. The sidecar reloads on its own, no restart. Until it returns, the old role is still serving traffic.\n      - region \"Point the application at it\": Command vault kv put secret/prod/db \\ username=api_rw_q3 \\ password=\"$NEW\" ./bin/wait-for-rollout \\ --key secret/prod/db \\ --timeout 120s Output all 14 pods on version 9 (47s)\n  - heading \"Verify — between steps 4 and 5\" [level=2]\n  - paragraph: Zero on the old role for five straight minutes. That is the bar.\n  - region \"Verify — between steps 4 and 5\": Command psql -h prod-pg-1 -c \"select usename, count(*) from pg_stat_activity where datname='app' group by 1\" Output api_rw_q3 | 42 api_rw_q2 | 0\n  - paragraph: \"If the old role still has connections: a pod missed the reload. Restart it.\"\n  - list:\n    - listitem:\n      - heading \"Retire the old role\" [level=3]\n      - paragraph: Only once the verification check has been clean for five minutes.\n      - region \"Retire the old role\": Command psql -h prod-pg-1 \\ -c \"drop role api_rw_q2\" Output DROP ROLE\n      - paragraph:\n        - text: \"If it says objects depend on it: something still owns tables as that role. Stop and escalate — never\"\n        - code: REASSIGN OWNED\n        - text: on production alone.\n  - heading \"If it goes wrong\" [level=2]\n  - heading \"Rollback — safe at any point before step 5\" [level=3]\n  - paragraph: The old secret version and the old role both still exist, so this is a sixty-second undo — not a deploy.\n  - region \"Rollback — safe at any point before step 5\": Command vault kv rollback -version=8 \\ secret/prod/db ./bin/wait-for-rollout \\ --key secret/prod/db Output all 14 pods back on version 8 (39s)\n  - heading \"When to escalate\" [level=2]\n  - list:\n    - listitem:\n      - text: Connections have not drained after 10 minutes. Page\n      - code: /page data-platform\n      - text: and leave both roles in place.\n    - listitem:\n      - code: DROP ROLE\n      - text: fails for any reason. Stop and page. Two live roles is safe; half-rotated is not.\n  - paragraph: rotate-db-credentials · owner data-platform · reviewed 28 Aug 2026 · last run 12 Jun 2026",
      "pass": true,
      "footerScreenshot": "runbook--sandbox-footer-390.png",
      "footerScreenshotSha256": "f5ce93dab42f7be576e707d1a50194ea4ff4cc7c8707620f1c19cf72486d5832"
    },
    {
      "slug": "runbook",
      "appWidth": 320,
      "javaScript": true,
      "sourceSha256": "64f9d4322e821044456be0a4ebd798339a9eb52d1ef16982759eba6995c965c7",
      "before": 6122,
      "innerWidth": 286,
      "scrollWidth": 286,
      "height": 6122,
      "proseBelow": [],
      "codeBelow": [],
      "roles": 0,
      "tabindexes": 0,
      "duplicateIds": [],
      "lists": [
        {
          "tag": "UL",
          "start": null,
          "count": 4
        },
        {
          "tag": "OL",
          "start": null,
          "count": 4
        },
        {
          "tag": "OL",
          "start": "5",
          "count": 1
        },
        {
          "tag": "UL",
          "start": null,
          "count": 2
        }
      ],
      "regions": [
        {
          "ids": "precondition-1",
          "labels": [
            "You are on the VPN and reach the primary."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "precondition-2",
          "labels": [
            "The deploy freeze is off."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "precondition-3",
          "labels": [
            "No migration is running."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "precondition-4",
          "labels": [
            "You can write to the secret store."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "command-1",
          "labels": [
            "Announce it"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "command-2",
          "labels": [
            "Generate the new secret"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "command-3",
          "labels": [
            "Create the successor role"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "command-4",
          "labels": [
            "Point the application at it"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "operation-section-1",
          "labels": [
            "Verify — between steps 4 and 5"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "command-6",
          "labels": [
            "Retire the old role"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "command-7",
          "labels": [
            "Rollback — safe at any point before step 5"
          ],
          "overflow": true,
          "tag": "SECTION"
        }
      ],
      "allDetailsOpen": true,
      "footer": {
        "top": 319.09375,
        "bottom": 444.46875,
        "innerHeight": 480
      },
      "ariaTree": "- main:\n  - paragraph: Runbook\n  - heading \"Rotate the production database credentials\" [level=1]\n  - paragraph:\n    - text: Quarterly rotation of\n    - code: api_rw\n    - text: \"on\"\n    - code: prod-pg-1\n    - text: . Zero downtime — but only if step 4 finishes before you touch step 5.\n  - term: When\n  - definition: Quarterly\n  - term: Takes\n  - definition: ~15 min\n  - term: Downtime\n  - definition: None\n  - term: Needs\n  - definition: db-admin\n  - heading \"Before you start\" [level=2]\n  - paragraph: All four. They take a minute together.\n  - list:\n    - listitem:\n      - text: You are on the VPN and reach the primary.\n      - region \"You are on the VPN and reach the primary.\":\n        - code: psql -h prod-pg-1 -c '\\conninfo'\n    - listitem:\n      - text: The deploy freeze is off.\n      - region \"The deploy freeze is off.\":\n        - code: \"./bin/freeze status # => open\"\n    - listitem:\n      - text: No migration is running.\n      - region \"No migration is running.\":\n        - code: \"./bin/migrations running # => 0\"\n    - listitem:\n      - text: You can write to the secret store.\n      - region \"You can write to the secret store.\":\n        - code: vault kv get -field=version \\ secret/prod/db\n  - paragraph: Stop if any is false Rotating during a migration leaves the migrator holding a dead connection. Wait it out.\n  - heading \"The procedure\" [level=2]\n  - paragraph: Two roles exist at once for a few minutes. That overlap is the design — nothing restarts, nothing has to be timed.\n  - list:\n    - listitem:\n      - heading \"Announce it\" [level=3]\n      - paragraph: A rotation nobody expected costs more than the one you announce.\n      - region \"Announce it\": \"Command ./bin/announce \\\"rotating api_rw, ~15 min, no downtime expected\\\" Output posted to #eng-oncall\"\n    - listitem:\n      - heading \"Generate the new secret\" [level=3]\n      - paragraph: Written to a staging slot the application does not read yet.\n      - region \"Generate the new secret\": Command export NEW=$(openssl rand -hex 24) vault kv put secret/prod/db/next \\ password=\"$NEW\" Output Key Value version 8\n    - listitem:\n      - heading \"Create the successor role\" [level=3]\n      - paragraph:\n        - text: It inherits from the\n        - code: api_rw\n        - text: group role, so you never re-grant table by table.\n      - region \"Create the successor role\": Command psql -h prod-pg-1 \\ -f sql/rotate.sql \\ -v pw=\"$NEW\" -v role=api_rw_q3 Output CREATE ROLE GRANT\n    - listitem:\n      - heading \"Point the application at it\" [level=3]\n      - paragraph: This is the step that matters. The sidecar reloads on its own, no restart. Until it returns, the old role is still serving traffic.\n      - region \"Point the application at it\": Command vault kv put secret/prod/db \\ username=api_rw_q3 \\ password=\"$NEW\" ./bin/wait-for-rollout \\ --key secret/prod/db \\ --timeout 120s Output all 14 pods on version 9 (47s)\n  - heading \"Verify — between steps 4 and 5\" [level=2]\n  - paragraph: Zero on the old role for five straight minutes. That is the bar.\n  - region \"Verify — between steps 4 and 5\": Command psql -h prod-pg-1 -c \"select usename, count(*) from pg_stat_activity where datname='app' group by 1\" Output api_rw_q3 | 42 api_rw_q2 | 0\n  - paragraph: \"If the old role still has connections: a pod missed the reload. Restart it.\"\n  - list:\n    - listitem:\n      - heading \"Retire the old role\" [level=3]\n      - paragraph: Only once the verification check has been clean for five minutes.\n      - region \"Retire the old role\": Command psql -h prod-pg-1 \\ -c \"drop role api_rw_q2\" Output DROP ROLE\n      - paragraph:\n        - text: \"If it says objects depend on it: something still owns tables as that role. Stop and escalate — never\"\n        - code: REASSIGN OWNED\n        - text: on production alone.\n  - heading \"If it goes wrong\" [level=2]\n  - heading \"Rollback — safe at any point before step 5\" [level=3]\n  - paragraph: The old secret version and the old role both still exist, so this is a sixty-second undo — not a deploy.\n  - region \"Rollback — safe at any point before step 5\": Command vault kv rollback -version=8 \\ secret/prod/db ./bin/wait-for-rollout \\ --key secret/prod/db Output all 14 pods back on version 8 (39s)\n  - heading \"When to escalate\" [level=2]\n  - list:\n    - listitem:\n      - text: Connections have not drained after 10 minutes. Page\n      - code: /page data-platform\n      - text: and leave both roles in place.\n    - listitem:\n      - code: DROP ROLE\n      - text: fails for any reason. Stop and page. Two live roles is safe; half-rotated is not.\n  - paragraph: rotate-db-credentials · owner data-platform · reviewed 28 Aug 2026 · last run 12 Jun 2026",
      "pass": true
    },
    {
      "slug": "changelog",
      "appWidth": 390,
      "javaScript": false,
      "sourceSha256": "77ff080efe080fc40813e626c7e8d217bd9d5e31d25b7fdeec8e539ef9bda124",
      "before": 3451,
      "innerWidth": 356,
      "scrollWidth": 356,
      "height": 4119,
      "proseBelow": [],
      "codeBelow": [],
      "roles": 0,
      "tabindexes": 0,
      "duplicateIds": [],
      "lists": [
        {
          "tag": "OL",
          "start": null,
          "count": 2
        },
        {
          "tag": "UL",
          "start": null,
          "count": 5
        },
        {
          "tag": "UL",
          "start": null,
          "count": 2
        },
        {
          "tag": "UL",
          "start": null,
          "count": 6
        }
      ],
      "regions": [],
      "allDetailsOpen": true,
      "footer": {
        "top": 331.453125,
        "bottom": 444.234375,
        "innerHeight": 480
      },
      "ariaTree": "- main:\n  - paragraph: Changelog\n  - heading \"Kestrel Platform\" [level=1]\n  - paragraph: Every user-visible change to the Kestrel API, CLI and dashboard. Dates are the day a release reached the stable channel.\n  - term: Latest\n  - definition: 4.3.0\n  - term: Released\n  - definition: 4 Sep 2026\n  - term: Channel\n  - definition: stable\n  - term: API version\n  - definition: 2026-06-01\n  - region \"! Breaking Two changes need action before you upgrade\":\n    - heading \"! Breaking Two changes need action before you upgrade\" [level=2]\n    - list:\n      - listitem:\n        - code: GET /v1/runs\n        - text: no longer returns\n        - code: steps[]\n        - text: inline. Fetch them from\n        - code: /v1/runs/:id/steps\n        - text: \", or pass\"\n        - code: \"?expand=steps\"\n        - text: for the old shape. The inline field is removed for good in 5.0.\n      - listitem:\n        - text: The CLI reads\n        - code: KESTREL_TOKEN\n        - text: \", not\"\n        - code: KESTREL_API_KEY\n        - text: . The old variable still works until 1 Dec 2026 and prints a deprecation warning on every invocation.\n        - code: kestrel doctor\n        - text: tells you which one you are on.\n  - region \"4.3.0\":\n    - heading \"4.3.0\" [level=2]\n    - paragraph: 4 Sep 2026\n    - paragraph: Current\n    - paragraph: Scheduled runs get a real backfill story, the run log stops truncating at 10k lines, and the dashboard finally remembers which workspace you were in.\n    - list:\n      - listitem:\n        - text: + Added\n        - heading \"Backfill for scheduled runs\" [level=3]\n        - paragraph:\n          - code: kestrel schedule backfill <id> --from 2026-08-01\n          - text: replays a schedule over a past window, at most 50 runs at a time, with the same concurrency limit as live execution.\n        - paragraph: \"#2841 · docs/schedules#backfill\"\n      - listitem:\n        - text: + Added\n        - heading \"Run log streaming over SSE\" [level=3]\n        - paragraph:\n          - code: GET /v1/runs/:id/logs?stream=true\n          - text: emits log lines as they are written. Logs are retained for 30 days on Team plans, 7 on Free.\n        - paragraph: \"#2803\"\n      - listitem:\n        - text: ↔ Changed\n        - heading \"Run logs keep the last 200k lines, not 10k\" [level=3]\n        - paragraph: Long-running jobs were silently losing their tail, which is precisely the part anyone reads. Older lines are now dropped from the head instead.\n        - paragraph: \"#2790 · reported by 14 workspaces\"\n      - listitem:\n        - text: ✓ Fixed\n        - heading \"Retries no longer double-count against the rate limit\" [level=3]\n        - paragraph: An automatic retry consumed two units of quota instead of one. Affected workspaces have had the difference credited back to the current billing period.\n        - paragraph: \"#2822 · severity: high\"\n      - listitem:\n        - text: − Removed\n        - heading \"The legacy /v0 endpoints are gone\" [level=3]:\n          - text: The legacy\n          - code: /v0\n          - text: endpoints are gone\n        - paragraph: Deprecated since March, at 0.02% of traffic for the last eight weeks. Every caller was contacted directly in August.\n        - paragraph: \"#2755 · announced 12 Mar 2026\"\n  - region \"4.2.2\":\n    - heading \"4.2.2\" [level=2]\n    - paragraph: 21 Aug 2026\n    - paragraph: Patch\n    - paragraph: \"A patch release: one regression from 4.2.0 and two small corrections.\"\n    - list:\n      - listitem:\n        - text: ✓ Fixed\n        - heading \"Webhook signatures verified against the wrong secret after rotation\" [level=3]\n        - paragraph: For roughly six hours after a secret was rotated, deliveries were signed with the retired key. Rotation now writes both keys and accepts either for 24 hours.\n        - paragraph: \"#2731 · postmortem published\"\n      - listitem:\n        - text: ↔ Changed\n        - heading \"Timeouts are reported in seconds everywhere\" [level=3]\n        - paragraph: The API returned milliseconds, the CLI printed seconds, and the dashboard mixed both.\n        - paragraph: \"#2724\"\n  - group: Six earlier releases, 4.0.0 – 4.2.1\n  - paragraph: Kestrel Platform · changelog maintained by the release team\n  - paragraph: Generated 4 Sep 2026, 09:12 UTC",
      "pass": true
    },
    {
      "slug": "changelog",
      "appWidth": 320,
      "javaScript": false,
      "sourceSha256": "77ff080efe080fc40813e626c7e8d217bd9d5e31d25b7fdeec8e539ef9bda124",
      "before": 4098,
      "innerWidth": 286,
      "scrollWidth": 286,
      "height": 4902,
      "proseBelow": [],
      "codeBelow": [],
      "roles": 0,
      "tabindexes": 0,
      "duplicateIds": [],
      "lists": [
        {
          "tag": "OL",
          "start": null,
          "count": 2
        },
        {
          "tag": "UL",
          "start": null,
          "count": 5
        },
        {
          "tag": "UL",
          "start": null,
          "count": 2
        },
        {
          "tag": "UL",
          "start": null,
          "count": 6
        }
      ],
      "regions": [],
      "allDetailsOpen": true,
      "footer": {
        "top": 330.890625,
        "bottom": 443.671875,
        "innerHeight": 480
      },
      "ariaTree": "- main:\n  - paragraph: Changelog\n  - heading \"Kestrel Platform\" [level=1]\n  - paragraph: Every user-visible change to the Kestrel API, CLI and dashboard. Dates are the day a release reached the stable channel.\n  - term: Latest\n  - definition: 4.3.0\n  - term: Released\n  - definition: 4 Sep 2026\n  - term: Channel\n  - definition: stable\n  - term: API version\n  - definition: 2026-06-01\n  - region \"! Breaking Two changes need action before you upgrade\":\n    - heading \"! Breaking Two changes need action before you upgrade\" [level=2]\n    - list:\n      - listitem:\n        - code: GET /v1/runs\n        - text: no longer returns\n        - code: steps[]\n        - text: inline. Fetch them from\n        - code: /v1/runs/:id/steps\n        - text: \", or pass\"\n        - code: \"?expand=steps\"\n        - text: for the old shape. The inline field is removed for good in 5.0.\n      - listitem:\n        - text: The CLI reads\n        - code: KESTREL_TOKEN\n        - text: \", not\"\n        - code: KESTREL_API_KEY\n        - text: . The old variable still works until 1 Dec 2026 and prints a deprecation warning on every invocation.\n        - code: kestrel doctor\n        - text: tells you which one you are on.\n  - region \"4.3.0\":\n    - heading \"4.3.0\" [level=2]\n    - paragraph: 4 Sep 2026\n    - paragraph: Current\n    - paragraph: Scheduled runs get a real backfill story, the run log stops truncating at 10k lines, and the dashboard finally remembers which workspace you were in.\n    - list:\n      - listitem:\n        - text: + Added\n        - heading \"Backfill for scheduled runs\" [level=3]\n        - paragraph:\n          - code: kestrel schedule backfill <id> --from 2026-08-01\n          - text: replays a schedule over a past window, at most 50 runs at a time, with the same concurrency limit as live execution.\n        - paragraph: \"#2841 · docs/schedules#backfill\"\n      - listitem:\n        - text: + Added\n        - heading \"Run log streaming over SSE\" [level=3]\n        - paragraph:\n          - code: GET /v1/runs/:id/logs?stream=true\n          - text: emits log lines as they are written. Logs are retained for 30 days on Team plans, 7 on Free.\n        - paragraph: \"#2803\"\n      - listitem:\n        - text: ↔ Changed\n        - heading \"Run logs keep the last 200k lines, not 10k\" [level=3]\n        - paragraph: Long-running jobs were silently losing their tail, which is precisely the part anyone reads. Older lines are now dropped from the head instead.\n        - paragraph: \"#2790 · reported by 14 workspaces\"\n      - listitem:\n        - text: ✓ Fixed\n        - heading \"Retries no longer double-count against the rate limit\" [level=3]\n        - paragraph: An automatic retry consumed two units of quota instead of one. Affected workspaces have had the difference credited back to the current billing period.\n        - paragraph: \"#2822 · severity: high\"\n      - listitem:\n        - text: − Removed\n        - heading \"The legacy /v0 endpoints are gone\" [level=3]:\n          - text: The legacy\n          - code: /v0\n          - text: endpoints are gone\n        - paragraph: Deprecated since March, at 0.02% of traffic for the last eight weeks. Every caller was contacted directly in August.\n        - paragraph: \"#2755 · announced 12 Mar 2026\"\n  - region \"4.2.2\":\n    - heading \"4.2.2\" [level=2]\n    - paragraph: 21 Aug 2026\n    - paragraph: Patch\n    - paragraph: \"A patch release: one regression from 4.2.0 and two small corrections.\"\n    - list:\n      - listitem:\n        - text: ✓ Fixed\n        - heading \"Webhook signatures verified against the wrong secret after rotation\" [level=3]\n        - paragraph: For roughly six hours after a secret was rotated, deliveries were signed with the retired key. Rotation now writes both keys and accepts either for 24 hours.\n        - paragraph: \"#2731 · postmortem published\"\n      - listitem:\n        - text: ↔ Changed\n        - heading \"Timeouts are reported in seconds everywhere\" [level=3]\n        - paragraph: The API returned milliseconds, the CLI printed seconds, and the dashboard mixed both.\n        - paragraph: \"#2724\"\n  - group: Six earlier releases, 4.0.0 – 4.2.1\n  - paragraph: Kestrel Platform · changelog maintained by the release team\n  - paragraph: Generated 4 Sep 2026, 09:12 UTC",
      "pass": true
    },
    {
      "slug": "migration-guide",
      "appWidth": 390,
      "javaScript": false,
      "sourceSha256": "f903fe929606e0538cd66a5ca3f316055a5b17edd808f3b6d2c66bc0431d8606",
      "before": 4450,
      "innerWidth": 356,
      "scrollWidth": 356,
      "height": 4690,
      "proseBelow": [],
      "codeBelow": [],
      "roles": 0,
      "tabindexes": 0,
      "duplicateIds": [],
      "lists": [
        {
          "tag": "UL",
          "start": null,
          "count": 3
        },
        {
          "tag": "OL",
          "start": null,
          "count": 3
        },
        {
          "tag": "UL",
          "start": null,
          "count": 3
        }
      ],
      "regions": [
        {
          "ids": "change-1 change-1-label-0",
          "labels": [
            "1 · The client takes an options object",
            "Before · v3"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "change-1 change-1-label-1",
          "labels": [
            "1 · The client takes an options object",
            "After · v4"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "change-2 change-2-label-0",
          "labels": [
            "2 · List calls return a page, not an array",
            "Before · v3"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "change-2 change-2-label-1",
          "labels": [
            "2 · List calls return a page, not an array",
            "After · v4"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "upgrade-1",
          "labels": [
            "Upgrade the dependency"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "upgrade-2",
          "labels": [
            "Run the codemod"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "upgrade-3",
          "labels": [
            "Verify before you deploy"
          ],
          "overflow": false,
          "tag": "SECTION"
        }
      ],
      "allDetailsOpen": true,
      "footer": {
        "top": 349.046875,
        "bottom": 443.640625,
        "innerHeight": 480
      },
      "ariaTree": "- main:\n  - paragraph: Migration guide\n  - heading \"Halyard SDK v3 → v4\" [level=1]\n  - paragraph: 3.8.2 → 4.1.0\n  - paragraph: \"Three breaking changes, one codemod, about twenty minutes. No coordinated deploy: v3 and v4 clients can talk to the API at the same time.\"\n  - text: Deadline\n  - paragraph:\n    - text: v3 stops receiving security fixes on\n    - strong: 1 December 2026\n    - text: — 88 days away.\n  - term: Applies to\n  - definition: JS / TS SDK\n  - term: Effort\n  - definition: ~20 min\n  - term: Breaking\n  - definition: 3 changes\n  - term: Rollback\n  - definition: Safe, any time\n  - heading \"What breaks, and what to write instead\" [level=2]\n  - article:\n    - heading \"1 · The client takes an options object\" [level=3]\n    - paragraph: A bare API-key string is no longer accepted, and the export is now named.\n    - paragraph: − Before · v3\n    - region \"1 · The client takes an options object − Before · v3\":\n      - code: import Halyard from \"halyard\"; const hal = new Halyard( process.env.HALYARD_API_KEY );\n    - paragraph: + After · v4\n    - region \"1 · The client takes an options object + After · v4\":\n      - code: \"import { Halyard } from \\\"halyard\\\"; const hal = new Halyard({ token: process.env.HALYARD_TOKEN, });\"\n  - article:\n    - heading \"2 · List calls return a page, not an array\" [level=3]\n    - paragraph:\n      - text: Every\n      - code: list*\n      - text: method returns\n      - code: \"{ data, cursor }\"\n      - text: . Iterate the client when you want all of it — it pages for you.\n    - paragraph: − Before · v3\n    - region \"2 · List calls return a page, not an array − Before · v3\":\n      - code: const runs = await hal.listRuns(); runs.map(r => r.id); // silently capped at 100\n    - paragraph: + After · v4\n    - region \"2 · List calls return a page, not an array + After · v4\":\n      - code: \"const page = await hal.listRuns(); page.data.map(r => r.id); for await (const r of hal.runs) {}\"\n  - article:\n    - heading \"3 · Three renames, no other behaviour change\" [level=3]\n    - paragraph: The codemod does all three; they are listed so you can grep for them yourself.\n    - paragraph: − v3 / + v4\n    - list:\n      - listitem: − err.code === 429 + err.type === \"rate_limited\"\n      - listitem: − hal.close() + await hal.dispose()\n      - listitem: − HALYARD_API_KEY + HALYARD_TOKEN\n  - heading \"Do it in three steps\" [level=2]\n  - list:\n    - listitem:\n      - heading \"Upgrade the dependency\" [level=3]\n      - region \"Upgrade the dependency\":\n        - code: $ npm install halyard@^4.1.0\n    - listitem:\n      - heading \"Run the codemod\" [level=3]\n      - paragraph:\n        - text: It rewrites constructors, renames and\n        - code: err.code\n        - text: comparisons. Where it is not sure it leaves a\n        - code: TODO(halyard)\n        - text: —\n        - code: grep -rn \"TODO(halyard)\" src/\n        - text: to find them. Pagination is the one only you can decide.\n      - region \"Run the codemod\":\n        - code: $ npx halyard-codemod v3-to-v4 src/ 42 files scanned 17 files changed 3 need a human · see TODO(halyard)\n    - listitem:\n      - heading \"Verify before you deploy\" [level=3]\n      - paragraph: Strict mode turns every remaining v3 shape into a thrown error instead of a warning, so the test run tells you the truth.\n      - region \"Verify before you deploy\":\n        - code: $ HALYARD_STRICT=1 npm test ✓ 184 passing ✓ 0 deprecation warnings\n  - heading \"If it goes wrong\" [level=2]\n  - paragraph:\n    - text: Two errors account for almost every report. Rolling back is reinstalling\n    - code: halyard@3.8.2\n    - text: and reverting the codemod commit.\n  - group: \"TypeError: runs.map is not a function\"\n  - group: \"HalyardError: missing_token\"\n  - heading \"The dates that matter\" [level=2]\n  - list:\n    - listitem: 12 Aug 2026 v4.0.0 released, v3 enters maintenance.\n    - listitem: 1 Dec 2026 v3 security fixes stop. Plan for this one.\n    - listitem: 1 Mar 2027 v3 unpublished, support ends.\n  - paragraph:\n    - text: Halyard SDK · migration guide revised 4 Sep 2026 ·\n    - link \"halyard.dev/docs/v4\":\n      - /url: https://halyard.dev/docs/v4",
      "pass": true
    },
    {
      "slug": "migration-guide",
      "appWidth": 320,
      "javaScript": false,
      "sourceSha256": "f903fe929606e0538cd66a5ca3f316055a5b17edd808f3b6d2c66bc0431d8606",
      "before": 4883,
      "innerWidth": 286,
      "scrollWidth": 286,
      "height": 5177,
      "proseBelow": [],
      "codeBelow": [],
      "roles": 0,
      "tabindexes": 0,
      "duplicateIds": [],
      "lists": [
        {
          "tag": "UL",
          "start": null,
          "count": 3
        },
        {
          "tag": "OL",
          "start": null,
          "count": 3
        },
        {
          "tag": "UL",
          "start": null,
          "count": 3
        }
      ],
      "regions": [
        {
          "ids": "change-1 change-1-label-0",
          "labels": [
            "1 · The client takes an options object",
            "Before · v3"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "change-1 change-1-label-1",
          "labels": [
            "1 · The client takes an options object",
            "After · v4"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "change-2 change-2-label-0",
          "labels": [
            "2 · List calls return a page, not an array",
            "Before · v3"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "change-2 change-2-label-1",
          "labels": [
            "2 · List calls return a page, not an array",
            "After · v4"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "upgrade-1",
          "labels": [
            "Upgrade the dependency"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "upgrade-2",
          "labels": [
            "Run the codemod"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "upgrade-3",
          "labels": [
            "Verify before you deploy"
          ],
          "overflow": true,
          "tag": "SECTION"
        }
      ],
      "allDetailsOpen": true,
      "footer": {
        "top": 324.078125,
        "bottom": 444.265625,
        "innerHeight": 480
      },
      "ariaTree": "- main:\n  - paragraph: Migration guide\n  - heading \"Halyard SDK v3 → v4\" [level=1]\n  - paragraph: 3.8.2 → 4.1.0\n  - paragraph: \"Three breaking changes, one codemod, about twenty minutes. No coordinated deploy: v3 and v4 clients can talk to the API at the same time.\"\n  - text: Deadline\n  - paragraph:\n    - text: v3 stops receiving security fixes on\n    - strong: 1 December 2026\n    - text: — 88 days away.\n  - term: Applies to\n  - definition: JS / TS SDK\n  - term: Effort\n  - definition: ~20 min\n  - term: Breaking\n  - definition: 3 changes\n  - term: Rollback\n  - definition: Safe, any time\n  - heading \"What breaks, and what to write instead\" [level=2]\n  - article:\n    - heading \"1 · The client takes an options object\" [level=3]\n    - paragraph: A bare API-key string is no longer accepted, and the export is now named.\n    - paragraph: − Before · v3\n    - region \"1 · The client takes an options object − Before · v3\":\n      - code: import Halyard from \"halyard\"; const hal = new Halyard( process.env.HALYARD_API_KEY );\n    - paragraph: + After · v4\n    - region \"1 · The client takes an options object + After · v4\":\n      - code: \"import { Halyard } from \\\"halyard\\\"; const hal = new Halyard({ token: process.env.HALYARD_TOKEN, });\"\n  - article:\n    - heading \"2 · List calls return a page, not an array\" [level=3]\n    - paragraph:\n      - text: Every\n      - code: list*\n      - text: method returns\n      - code: \"{ data, cursor }\"\n      - text: . Iterate the client when you want all of it — it pages for you.\n    - paragraph: − Before · v3\n    - region \"2 · List calls return a page, not an array − Before · v3\":\n      - code: const runs = await hal.listRuns(); runs.map(r => r.id); // silently capped at 100\n    - paragraph: + After · v4\n    - region \"2 · List calls return a page, not an array + After · v4\":\n      - code: \"const page = await hal.listRuns(); page.data.map(r => r.id); for await (const r of hal.runs) {}\"\n  - article:\n    - heading \"3 · Three renames, no other behaviour change\" [level=3]\n    - paragraph: The codemod does all three; they are listed so you can grep for them yourself.\n    - paragraph: − v3 / + v4\n    - list:\n      - listitem: − err.code === 429 + err.type === \"rate_limited\"\n      - listitem: − hal.close() + await hal.dispose()\n      - listitem: − HALYARD_API_KEY + HALYARD_TOKEN\n  - heading \"Do it in three steps\" [level=2]\n  - list:\n    - listitem:\n      - heading \"Upgrade the dependency\" [level=3]\n      - region \"Upgrade the dependency\":\n        - code: $ npm install halyard@^4.1.0\n    - listitem:\n      - heading \"Run the codemod\" [level=3]\n      - paragraph:\n        - text: It rewrites constructors, renames and\n        - code: err.code\n        - text: comparisons. Where it is not sure it leaves a\n        - code: TODO(halyard)\n        - text: —\n        - code: grep -rn \"TODO(halyard)\" src/\n        - text: to find them. Pagination is the one only you can decide.\n      - region \"Run the codemod\":\n        - code: $ npx halyard-codemod v3-to-v4 src/ 42 files scanned 17 files changed 3 need a human · see TODO(halyard)\n    - listitem:\n      - heading \"Verify before you deploy\" [level=3]\n      - paragraph: Strict mode turns every remaining v3 shape into a thrown error instead of a warning, so the test run tells you the truth.\n      - region \"Verify before you deploy\":\n        - code: $ HALYARD_STRICT=1 npm test ✓ 184 passing ✓ 0 deprecation warnings\n  - heading \"If it goes wrong\" [level=2]\n  - paragraph:\n    - text: Two errors account for almost every report. Rolling back is reinstalling\n    - code: halyard@3.8.2\n    - text: and reverting the codemod commit.\n  - group: \"TypeError: runs.map is not a function\"\n  - group: \"HalyardError: missing_token\"\n  - heading \"The dates that matter\" [level=2]\n  - list:\n    - listitem: 12 Aug 2026 v4.0.0 released, v3 enters maintenance.\n    - listitem: 1 Dec 2026 v3 security fixes stop. Plan for this one.\n    - listitem: 1 Mar 2027 v3 unpublished, support ends.\n  - paragraph:\n    - text: Halyard SDK · migration guide revised 4 Sep 2026 ·\n    - link \"halyard.dev/docs/v4\":\n      - /url: https://halyard.dev/docs/v4",
      "pass": true
    },
    {
      "slug": "runbook",
      "appWidth": 390,
      "javaScript": false,
      "sourceSha256": "64f9d4322e821044456be0a4ebd798339a9eb52d1ef16982759eba6995c965c7",
      "before": 5423,
      "innerWidth": 356,
      "scrollWidth": 356,
      "height": 5423,
      "proseBelow": [],
      "codeBelow": [],
      "roles": 0,
      "tabindexes": 0,
      "duplicateIds": [],
      "lists": [
        {
          "tag": "UL",
          "start": null,
          "count": 4
        },
        {
          "tag": "OL",
          "start": null,
          "count": 4
        },
        {
          "tag": "OL",
          "start": "5",
          "count": 1
        },
        {
          "tag": "UL",
          "start": null,
          "count": 2
        }
      ],
      "regions": [
        {
          "ids": "precondition-1",
          "labels": [
            "You are on the VPN and reach the primary."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "precondition-2",
          "labels": [
            "The deploy freeze is off."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "precondition-3",
          "labels": [
            "No migration is running."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "precondition-4",
          "labels": [
            "You can write to the secret store."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "command-1",
          "labels": [
            "Announce it"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "command-2",
          "labels": [
            "Generate the new secret"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "command-3",
          "labels": [
            "Create the successor role"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "command-4",
          "labels": [
            "Point the application at it"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "operation-section-1",
          "labels": [
            "Verify — between steps 4 and 5"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "command-6",
          "labels": [
            "Retire the old role"
          ],
          "overflow": false,
          "tag": "SECTION"
        },
        {
          "ids": "command-7",
          "labels": [
            "Rollback — safe at any point before step 5"
          ],
          "overflow": true,
          "tag": "SECTION"
        }
      ],
      "allDetailsOpen": true,
      "footer": {
        "top": 343.96875,
        "bottom": 443.75,
        "innerHeight": 480
      },
      "ariaTree": "- main:\n  - paragraph: Runbook\n  - heading \"Rotate the production database credentials\" [level=1]\n  - paragraph:\n    - text: Quarterly rotation of\n    - code: api_rw\n    - text: \"on\"\n    - code: prod-pg-1\n    - text: . Zero downtime — but only if step 4 finishes before you touch step 5.\n  - term: When\n  - definition: Quarterly\n  - term: Takes\n  - definition: ~15 min\n  - term: Downtime\n  - definition: None\n  - term: Needs\n  - definition: db-admin\n  - heading \"Before you start\" [level=2]\n  - paragraph: All four. They take a minute together.\n  - list:\n    - listitem:\n      - text: You are on the VPN and reach the primary.\n      - region \"You are on the VPN and reach the primary.\":\n        - code: psql -h prod-pg-1 -c '\\conninfo'\n    - listitem:\n      - text: The deploy freeze is off.\n      - region \"The deploy freeze is off.\":\n        - code: \"./bin/freeze status # => open\"\n    - listitem:\n      - text: No migration is running.\n      - region \"No migration is running.\":\n        - code: \"./bin/migrations running # => 0\"\n    - listitem:\n      - text: You can write to the secret store.\n      - region \"You can write to the secret store.\":\n        - code: vault kv get -field=version \\ secret/prod/db\n  - paragraph: Stop if any is false Rotating during a migration leaves the migrator holding a dead connection. Wait it out.\n  - heading \"The procedure\" [level=2]\n  - paragraph: Two roles exist at once for a few minutes. That overlap is the design — nothing restarts, nothing has to be timed.\n  - list:\n    - listitem:\n      - heading \"Announce it\" [level=3]\n      - paragraph: A rotation nobody expected costs more than the one you announce.\n      - region \"Announce it\": \"Command ./bin/announce \\\"rotating api_rw, ~15 min, no downtime expected\\\" Output posted to #eng-oncall\"\n    - listitem:\n      - heading \"Generate the new secret\" [level=3]\n      - paragraph: Written to a staging slot the application does not read yet.\n      - region \"Generate the new secret\": Command export NEW=$(openssl rand -hex 24) vault kv put secret/prod/db/next \\ password=\"$NEW\" Output Key Value version 8\n    - listitem:\n      - heading \"Create the successor role\" [level=3]\n      - paragraph:\n        - text: It inherits from the\n        - code: api_rw\n        - text: group role, so you never re-grant table by table.\n      - region \"Create the successor role\": Command psql -h prod-pg-1 \\ -f sql/rotate.sql \\ -v pw=\"$NEW\" -v role=api_rw_q3 Output CREATE ROLE GRANT\n    - listitem:\n      - heading \"Point the application at it\" [level=3]\n      - paragraph: This is the step that matters. The sidecar reloads on its own, no restart. Until it returns, the old role is still serving traffic.\n      - region \"Point the application at it\": Command vault kv put secret/prod/db \\ username=api_rw_q3 \\ password=\"$NEW\" ./bin/wait-for-rollout \\ --key secret/prod/db \\ --timeout 120s Output all 14 pods on version 9 (47s)\n  - heading \"Verify — between steps 4 and 5\" [level=2]\n  - paragraph: Zero on the old role for five straight minutes. That is the bar.\n  - region \"Verify — between steps 4 and 5\": Command psql -h prod-pg-1 -c \"select usename, count(*) from pg_stat_activity where datname='app' group by 1\" Output api_rw_q3 | 42 api_rw_q2 | 0\n  - paragraph: \"If the old role still has connections: a pod missed the reload. Restart it.\"\n  - list:\n    - listitem:\n      - heading \"Retire the old role\" [level=3]\n      - paragraph: Only once the verification check has been clean for five minutes.\n      - region \"Retire the old role\": Command psql -h prod-pg-1 \\ -c \"drop role api_rw_q2\" Output DROP ROLE\n      - paragraph:\n        - text: \"If it says objects depend on it: something still owns tables as that role. Stop and escalate — never\"\n        - code: REASSIGN OWNED\n        - text: on production alone.\n  - heading \"If it goes wrong\" [level=2]\n  - heading \"Rollback — safe at any point before step 5\" [level=3]\n  - paragraph: The old secret version and the old role both still exist, so this is a sixty-second undo — not a deploy.\n  - region \"Rollback — safe at any point before step 5\": Command vault kv rollback -version=8 \\ secret/prod/db ./bin/wait-for-rollout \\ --key secret/prod/db Output all 14 pods back on version 8 (39s)\n  - heading \"When to escalate\" [level=2]\n  - list:\n    - listitem:\n      - text: Connections have not drained after 10 minutes. Page\n      - code: /page data-platform\n      - text: and leave both roles in place.\n    - listitem:\n      - code: DROP ROLE\n      - text: fails for any reason. Stop and page. Two live roles is safe; half-rotated is not.\n  - paragraph: rotate-db-credentials · owner data-platform · reviewed 28 Aug 2026 · last run 12 Jun 2026",
      "pass": true
    },
    {
      "slug": "runbook",
      "appWidth": 320,
      "javaScript": false,
      "sourceSha256": "64f9d4322e821044456be0a4ebd798339a9eb52d1ef16982759eba6995c965c7",
      "before": 6122,
      "innerWidth": 286,
      "scrollWidth": 286,
      "height": 6122,
      "proseBelow": [],
      "codeBelow": [],
      "roles": 0,
      "tabindexes": 0,
      "duplicateIds": [],
      "lists": [
        {
          "tag": "UL",
          "start": null,
          "count": 4
        },
        {
          "tag": "OL",
          "start": null,
          "count": 4
        },
        {
          "tag": "OL",
          "start": "5",
          "count": 1
        },
        {
          "tag": "UL",
          "start": null,
          "count": 2
        }
      ],
      "regions": [
        {
          "ids": "precondition-1",
          "labels": [
            "You are on the VPN and reach the primary."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "precondition-2",
          "labels": [
            "The deploy freeze is off."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "precondition-3",
          "labels": [
            "No migration is running."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "precondition-4",
          "labels": [
            "You can write to the secret store."
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "command-1",
          "labels": [
            "Announce it"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "command-2",
          "labels": [
            "Generate the new secret"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "command-3",
          "labels": [
            "Create the successor role"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "command-4",
          "labels": [
            "Point the application at it"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "operation-section-1",
          "labels": [
            "Verify — between steps 4 and 5"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "command-6",
          "labels": [
            "Retire the old role"
          ],
          "overflow": true,
          "tag": "SECTION"
        },
        {
          "ids": "command-7",
          "labels": [
            "Rollback — safe at any point before step 5"
          ],
          "overflow": true,
          "tag": "SECTION"
        }
      ],
      "allDetailsOpen": true,
      "footer": {
        "top": 319.09375,
        "bottom": 444.46875,
        "innerHeight": 480
      },
      "ariaTree": "- main:\n  - paragraph: Runbook\n  - heading \"Rotate the production database credentials\" [level=1]\n  - paragraph:\n    - text: Quarterly rotation of\n    - code: api_rw\n    - text: \"on\"\n    - code: prod-pg-1\n    - text: . Zero downtime — but only if step 4 finishes before you touch step 5.\n  - term: When\n  - definition: Quarterly\n  - term: Takes\n  - definition: ~15 min\n  - term: Downtime\n  - definition: None\n  - term: Needs\n  - definition: db-admin\n  - heading \"Before you start\" [level=2]\n  - paragraph: All four. They take a minute together.\n  - list:\n    - listitem:\n      - text: You are on the VPN and reach the primary.\n      - region \"You are on the VPN and reach the primary.\":\n        - code: psql -h prod-pg-1 -c '\\conninfo'\n    - listitem:\n      - text: The deploy freeze is off.\n      - region \"The deploy freeze is off.\":\n        - code: \"./bin/freeze status # => open\"\n    - listitem:\n      - text: No migration is running.\n      - region \"No migration is running.\":\n        - code: \"./bin/migrations running # => 0\"\n    - listitem:\n      - text: You can write to the secret store.\n      - region \"You can write to the secret store.\":\n        - code: vault kv get -field=version \\ secret/prod/db\n  - paragraph: Stop if any is false Rotating during a migration leaves the migrator holding a dead connection. Wait it out.\n  - heading \"The procedure\" [level=2]\n  - paragraph: Two roles exist at once for a few minutes. That overlap is the design — nothing restarts, nothing has to be timed.\n  - list:\n    - listitem:\n      - heading \"Announce it\" [level=3]\n      - paragraph: A rotation nobody expected costs more than the one you announce.\n      - region \"Announce it\": \"Command ./bin/announce \\\"rotating api_rw, ~15 min, no downtime expected\\\" Output posted to #eng-oncall\"\n    - listitem:\n      - heading \"Generate the new secret\" [level=3]\n      - paragraph: Written to a staging slot the application does not read yet.\n      - region \"Generate the new secret\": Command export NEW=$(openssl rand -hex 24) vault kv put secret/prod/db/next \\ password=\"$NEW\" Output Key Value version 8\n    - listitem:\n      - heading \"Create the successor role\" [level=3]\n      - paragraph:\n        - text: It inherits from the\n        - code: api_rw\n        - text: group role, so you never re-grant table by table.\n      - region \"Create the successor role\": Command psql -h prod-pg-1 \\ -f sql/rotate.sql \\ -v pw=\"$NEW\" -v role=api_rw_q3 Output CREATE ROLE GRANT\n    - listitem:\n      - heading \"Point the application at it\" [level=3]\n      - paragraph: This is the step that matters. The sidecar reloads on its own, no restart. Until it returns, the old role is still serving traffic.\n      - region \"Point the application at it\": Command vault kv put secret/prod/db \\ username=api_rw_q3 \\ password=\"$NEW\" ./bin/wait-for-rollout \\ --key secret/prod/db \\ --timeout 120s Output all 14 pods on version 9 (47s)\n  - heading \"Verify — between steps 4 and 5\" [level=2]\n  - paragraph: Zero on the old role for five straight minutes. That is the bar.\n  - region \"Verify — between steps 4 and 5\": Command psql -h prod-pg-1 -c \"select usename, count(*) from pg_stat_activity where datname='app' group by 1\" Output api_rw_q3 | 42 api_rw_q2 | 0\n  - paragraph: \"If the old role still has connections: a pod missed the reload. Restart it.\"\n  - list:\n    - listitem:\n      - heading \"Retire the old role\" [level=3]\n      - paragraph: Only once the verification check has been clean for five minutes.\n      - region \"Retire the old role\": Command psql -h prod-pg-1 \\ -c \"drop role api_rw_q2\" Output DROP ROLE\n      - paragraph:\n        - text: \"If it says objects depend on it: something still owns tables as that role. Stop and escalate — never\"\n        - code: REASSIGN OWNED\n        - text: on production alone.\n  - heading \"If it goes wrong\" [level=2]\n  - heading \"Rollback — safe at any point before step 5\" [level=3]\n  - paragraph: The old secret version and the old role both still exist, so this is a sixty-second undo — not a deploy.\n  - region \"Rollback — safe at any point before step 5\": Command vault kv rollback -version=8 \\ secret/prod/db ./bin/wait-for-rollout \\ --key secret/prod/db Output all 14 pods back on version 8 (39s)\n  - heading \"When to escalate\" [level=2]\n  - list:\n    - listitem:\n      - text: Connections have not drained after 10 minutes. Page\n      - code: /page data-platform\n      - text: and leave both roles in place.\n    - listitem:\n      - code: DROP ROLE\n      - text: fails for any reason. Stop and page. Two live roles is safe; half-rotated is not.\n  - paragraph: rotate-db-credentials · owner data-platform · reviewed 28 Aug 2026 · last run 12 Jun 2026",
      "pass": true
    }
  ],
  "failures": [],
  "pass": true,
  "limitations": "Chromium actual app sandbox. Native disclosures exercised with keyboard with JS both enabled and disabled. Read-only sample rendering; no command execution."
}
