{
  "templates": [
    {
      "slug": "changelog",
      "canonicalSha256": "77ff080efe080fc40813e626c7e8d217bd9d5e31d25b7fdeec8e539ef9bda124",
      "sourceTextNodes": 87,
      "numericOccurrences": 58,
      "checks": {
        "allOtherBytesExact": true,
        "cssExact": true,
        "allTextExceptAuthorizedSentence": true,
        "numbers": true,
        "records": true,
        "codeDescendantsExact": true,
        "links": true,
        "noNewRolesOrTabindex": true,
        "noSvgMeterRemoved": true
      },
      "baselineRecords": {
        "breaking": [
          "GET /v1/runs no longer returns steps[] inline. Fetch them from /v1/runs/:id/steps, or pass ?expand=steps for the old shape. The inline field is removed for good in 5.0.",
          "The CLI reads KESTREL_TOKEN, not KESTREL_API_KEY. The old variable still works until 1 Dec 2026 and prints a deprecation warning on every invocation. kestrel doctor tells you which one you are on."
        ],
        "releases": [
          [
            "4.3.0",
            "4 Sep 2026",
            "Current",
            "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.",
            [
              [
                "Added",
                "Backfill for scheduled runs",
                [
                  "kestrel schedule backfill <id> --from 2026-08-01 replays a schedule over a past window, at most 50 runs at a time, with the same concurrency limit as live execution.",
                  "#2841 \u00b7 docs/schedules#backfill"
                ]
              ],
              [
                "Added",
                "Run log streaming over SSE",
                [
                  "GET /v1/runs/:id/logs?stream=true emits log lines as they are written. Logs are retained for 30 days on Team plans, 7 on Free.",
                  "#2803"
                ]
              ],
              [
                "Changed",
                "Run logs keep the last 200k lines, not 10k",
                [
                  "Long-running jobs were silently losing their tail, which is precisely the part anyone reads. Older lines are now dropped from the head instead.",
                  "#2790 \u00b7 reported by 14 workspaces"
                ]
              ],
              [
                "Fixed",
                "Retries no longer double-count against the rate limit",
                [
                  "An automatic retry consumed two units of quota instead of one. Affected workspaces have had the difference credited back to the current billing period.",
                  "#2822 \u00b7 severity: high"
                ]
              ],
              [
                "Removed",
                "The legacy /v0 endpoints are gone",
                [
                  "Deprecated since March, at 0.02% of traffic for the last eight weeks. Every caller was contacted directly in August.",
                  "#2755 \u00b7 announced 12 Mar 2026"
                ]
              ]
            ]
          ],
          [
            "4.2.2",
            "21 Aug 2026",
            "Patch",
            "A patch release: one regression from 4.2.0 and two small corrections.",
            [
              [
                "Fixed",
                "Webhook signatures verified against the wrong secret after rotation",
                [
                  "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.",
                  "#2731 \u00b7 postmortem published"
                ]
              ],
              [
                "Changed",
                "Timeouts are reported in seconds everywhere",
                [
                  "The API returned milliseconds, the CLI printed seconds, and the dashboard mixed both.",
                  "#2724"
                ]
              ]
            ]
          ]
        ],
        "archive": [
          "4.2.1Hotfix: dashboard failed to load for workspaces with over 500 schedules.",
          "4.2.0Schedules, per-workspace concurrency limits, and the run timeline view.",
          "4.1.2Fixed token refresh in the Python SDK; CLI binaries signed for macOS 15.",
          "4.1.0Webhooks with signed payloads and per-event delivery history.",
          "4.0.1Fixed the migration that left duplicated rows in run_steps.",
          "4.0.0The 2026-06-01 API version, workspaces, and the rewritten CLI."
        ],
        "meta": [
          "Latest4.3.0",
          "Released4 Sep 2026",
          "Channelstable",
          "API version2026-06-01"
        ]
      },
      "canonicalRecords": {
        "breaking": [
          "GET /v1/runs no longer returns steps[] inline. Fetch them from /v1/runs/:id/steps, or pass ?expand=steps for the old shape. The inline field is removed for good in 5.0.",
          "The CLI reads KESTREL_TOKEN, not KESTREL_API_KEY. The old variable still works until 1 Dec 2026 and prints a deprecation warning on every invocation. kestrel doctor tells you which one you are on."
        ],
        "releases": [
          [
            "4.3.0",
            "4 Sep 2026",
            "Current",
            "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.",
            [
              [
                "Added",
                "Backfill for scheduled runs",
                [
                  "kestrel schedule backfill <id> --from 2026-08-01 replays a schedule over a past window, at most 50 runs at a time, with the same concurrency limit as live execution.",
                  "#2841 \u00b7 docs/schedules#backfill"
                ]
              ],
              [
                "Added",
                "Run log streaming over SSE",
                [
                  "GET /v1/runs/:id/logs?stream=true emits log lines as they are written. Logs are retained for 30 days on Team plans, 7 on Free.",
                  "#2803"
                ]
              ],
              [
                "Changed",
                "Run logs keep the last 200k lines, not 10k",
                [
                  "Long-running jobs were silently losing their tail, which is precisely the part anyone reads. Older lines are now dropped from the head instead.",
                  "#2790 \u00b7 reported by 14 workspaces"
                ]
              ],
              [
                "Fixed",
                "Retries no longer double-count against the rate limit",
                [
                  "An automatic retry consumed two units of quota instead of one. Affected workspaces have had the difference credited back to the current billing period.",
                  "#2822 \u00b7 severity: high"
                ]
              ],
              [
                "Removed",
                "The legacy /v0 endpoints are gone",
                [
                  "Deprecated since March, at 0.02% of traffic for the last eight weeks. Every caller was contacted directly in August.",
                  "#2755 \u00b7 announced 12 Mar 2026"
                ]
              ]
            ]
          ],
          [
            "4.2.2",
            "21 Aug 2026",
            "Patch",
            "A patch release: one regression from 4.2.0 and two small corrections.",
            [
              [
                "Fixed",
                "Webhook signatures verified against the wrong secret after rotation",
                [
                  "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.",
                  "#2731 \u00b7 postmortem published"
                ]
              ],
              [
                "Changed",
                "Timeouts are reported in seconds everywhere",
                [
                  "The API returned milliseconds, the CLI printed seconds, and the dashboard mixed both.",
                  "#2724"
                ]
              ]
            ]
          ]
        ],
        "archive": [
          "4.2.1Hotfix: dashboard failed to load for workspaces with over 500 schedules.",
          "4.2.0Schedules, per-workspace concurrency limits, and the run timeline view.",
          "4.1.2Fixed token refresh in the Python SDK; CLI binaries signed for macOS 15.",
          "4.1.0Webhooks with signed payloads and per-event delivery history.",
          "4.0.1Fixed the migration that left duplicated rows in run_steps.",
          "4.0.0The 2026-06-01 API version, workspaces, and the rewritten CLI."
        ],
        "meta": [
          "Latest4.3.0",
          "Released4 Sep 2026",
          "Channelstable",
          "API version2026-06-01"
        ]
      },
      "codeInventory": [
        [
          "code",
          "/v0",
          []
        ],
        [
          "code",
          "/v1/runs/:id/steps",
          []
        ],
        [
          "code",
          "?expand=steps",
          []
        ],
        [
          "code",
          "GET /v1/runs",
          []
        ],
        [
          "code",
          "GET /v1/runs/:id/logs?stream=true",
          []
        ],
        [
          "code",
          "KESTREL_API_KEY",
          []
        ],
        [
          "code",
          "KESTREL_TOKEN",
          []
        ],
        [
          "code",
          "kestrel doctor",
          []
        ],
        [
          "code",
          "kestrel schedule backfill <id> --from 2026-08-01",
          []
        ],
        [
          "code",
          "run_steps",
          []
        ],
        [
          "code",
          "steps[]",
          []
        ]
      ],
      "authorizedSentenceChange": false
    },
    {
      "slug": "migration-guide",
      "canonicalSha256": "f903fe929606e0538cd66a5ca3f316055a5b17edd808f3b6d2c66bc0431d8606",
      "sourceTextNodes": 90,
      "numericOccurrences": 48,
      "checks": {
        "allOtherBytesExact": true,
        "cssExact": true,
        "allTextExceptAuthorizedSentence": true,
        "numbers": true,
        "records": true,
        "codeDescendantsExact": true,
        "links": true,
        "noNewRolesOrTabindex": true,
        "noSvgMeterRemoved": true
      },
      "baselineRecords": {
        "changes": [
          [
            "1 \u00b7 The client takes an options object",
            "A bare API-key string is no longer accepted, and the export is now named.",
            [
              [
                "Before \u00b7 v3",
                "import Halyard from \"halyard\";\nconst hal = new Halyard(\n  process.env.HALYARD_API_KEY\n);"
              ],
              [
                "After \u00b7 v4",
                "import { Halyard } from \"halyard\";\nconst hal = new Halyard({\n  token: process.env.HALYARD_TOKEN,\n});"
              ]
            ]
          ],
          [
            "2 \u00b7 List calls return a page, not an array",
            "Every list* method returns { data, cursor }. Iterate the client when you want all of it \u2014 it pages for you.",
            [
              [
                "Before \u00b7 v3",
                "const runs = await hal.listRuns();\nruns.map(r => r.id);\n// silently capped at 100"
              ],
              [
                "After \u00b7 v4",
                "const page = await hal.listRuns();\npage.data.map(r => r.id);\nfor await (const r of hal.runs) {}"
              ]
            ]
          ],
          [
            "3 \u00b7 Three renames, no other behaviour change",
            "The codemod does all three; they are listed so you can grep for them yourself.",
            [
              [
                "\u2212 v3 / + v4",
                [
                  [
                    "err.code === 429",
                    "err.type === \"rate_limited\""
                  ],
                  [
                    "hal.close()",
                    "await hal.dispose()"
                  ],
                  [
                    "HALYARD_API_KEY",
                    "HALYARD_TOKEN"
                  ]
                ]
              ]
            ]
          ]
        ],
        "steps": [
          [
            "Upgrade the dependency",
            [],
            "$ npm install halyard@^4.1.0"
          ],
          [
            "Run the codemod",
            [
              "It rewrites constructors, renames and err.code comparisons. Where it is not sure it leaves a TODO(halyard) \u2014 grep -rn \"TODO(halyard)\" src/ to find them. Pagination is the one only you can decide."
            ],
            "$ npx halyard-codemod v3-to-v4 src/\n  42 files scanned\n  17 files changed\n   3 need a human \u00b7 see TODO(halyard)"
          ],
          [
            "Verify before you deploy",
            [
              "Strict mode turns every remaining v3 shape into a thrown error instead of a warning, so the test run tells you the truth."
            ],
            "$ HALYARD_STRICT=1 npm test\n  \u2713 184 passing\n  \u2713 0 deprecation warnings"
          ]
        ],
        "errors": [
          [
            "TypeError: runs.map is not a function",
            "A list call you did not update. Use page.data, or iterate hal.runs if you actually wanted every row."
          ],
          [
            "HalyardError: missing_token",
            "The process still exports HALYARD_API_KEY. Rename it in your deployment config \u2014 v4 does not read the old name, on purpose."
          ]
        ],
        "dates": [
          "12 Aug 2026v4.0.0 released, v3 enters maintenance.",
          "1 Dec 2026v3 security fixes stop. Plan for this one.",
          "1 Mar 2027v3 unpublished, support ends."
        ],
        "facts": [
          "Applies toJS / TS SDK",
          "Effort~20 min",
          "Breaking3 changes",
          "RollbackSafe, any time"
        ]
      },
      "canonicalRecords": {
        "changes": [
          [
            "1 \u00b7 The client takes an options object",
            "A bare API-key string is no longer accepted, and the export is now named.",
            [
              [
                "Before \u00b7 v3",
                "import Halyard from \"halyard\";\nconst hal = new Halyard(\n  process.env.HALYARD_API_KEY\n);"
              ],
              [
                "After \u00b7 v4",
                "import { Halyard } from \"halyard\";\nconst hal = new Halyard({\n  token: process.env.HALYARD_TOKEN,\n});"
              ]
            ]
          ],
          [
            "2 \u00b7 List calls return a page, not an array",
            "Every list* method returns { data, cursor }. Iterate the client when you want all of it \u2014 it pages for you.",
            [
              [
                "Before \u00b7 v3",
                "const runs = await hal.listRuns();\nruns.map(r => r.id);\n// silently capped at 100"
              ],
              [
                "After \u00b7 v4",
                "const page = await hal.listRuns();\npage.data.map(r => r.id);\nfor await (const r of hal.runs) {}"
              ]
            ]
          ],
          [
            "3 \u00b7 Three renames, no other behaviour change",
            "The codemod does all three; they are listed so you can grep for them yourself.",
            [
              [
                "\u2212 v3 / + v4",
                [
                  [
                    "err.code === 429",
                    "err.type === \"rate_limited\""
                  ],
                  [
                    "hal.close()",
                    "await hal.dispose()"
                  ],
                  [
                    "HALYARD_API_KEY",
                    "HALYARD_TOKEN"
                  ]
                ]
              ]
            ]
          ]
        ],
        "steps": [
          [
            "Upgrade the dependency",
            [],
            "$ npm install halyard@^4.1.0"
          ],
          [
            "Run the codemod",
            [
              "It rewrites constructors, renames and err.code comparisons. Where it is not sure it leaves a TODO(halyard) \u2014 grep -rn \"TODO(halyard)\" src/ to find them. Pagination is the one only you can decide."
            ],
            "$ npx halyard-codemod v3-to-v4 src/\n  42 files scanned\n  17 files changed\n   3 need a human \u00b7 see TODO(halyard)"
          ],
          [
            "Verify before you deploy",
            [
              "Strict mode turns every remaining v3 shape into a thrown error instead of a warning, so the test run tells you the truth."
            ],
            "$ HALYARD_STRICT=1 npm test\n  \u2713 184 passing\n  \u2713 0 deprecation warnings"
          ]
        ],
        "errors": [
          [
            "TypeError: runs.map is not a function",
            "A list call you did not update. Use page.data, or iterate hal.runs if you actually wanted every row."
          ],
          [
            "HalyardError: missing_token",
            "The process still exports HALYARD_API_KEY. Rename it in your deployment config \u2014 v4 does not read the old name, on purpose."
          ]
        ],
        "dates": [
          "12 Aug 2026v4.0.0 released, v3 enters maintenance.",
          "1 Dec 2026v3 security fixes stop. Plan for this one.",
          "1 Mar 2027v3 unpublished, support ends."
        ],
        "facts": [
          "Applies toJS / TS SDK",
          "Effort~20 min",
          "Breaking3 changes",
          "RollbackSafe, any time"
        ]
      },
      "codeInventory": [
        [
          "code",
          "$ HALYARD_STRICT=1 npm test\n  \u2713 184 passing\n  \u2713 0 deprecation warnings",
          [
            [
              "span",
              "p",
              "$"
            ],
            [
              "span",
              "o",
              "  \u2713 184 passing\n  \u2713 0 deprecation warnings"
            ]
          ]
        ],
        [
          "code",
          "$ npm install halyard@^4.1.0",
          [
            [
              "span",
              "p",
              "$"
            ]
          ]
        ],
        [
          "code",
          "$ npx halyard-codemod v3-to-v4 src/\n  42 files scanned\n  17 files changed\n   3 need a human \u00b7 see TODO(halyard)",
          [
            [
              "span",
              "p",
              "$"
            ],
            [
              "span",
              "o",
              "  42 files scanned\n  17 files changed\n   3 need a human \u00b7 see TODO(halyard)"
            ]
          ]
        ],
        [
          "code",
          "HALYARD_API_KEY",
          []
        ],
        [
          "code",
          "TODO(halyard)",
          []
        ],
        [
          "code",
          "err.code",
          []
        ],
        [
          "code",
          "grep -rn \"TODO(halyard)\" src/",
          []
        ],
        [
          "code",
          "hal.runs",
          []
        ],
        [
          "code",
          "halyard@3.8.2",
          []
        ],
        [
          "code",
          "list",
          []
        ],
        [
          "code",
          "list*",
          []
        ],
        [
          "code",
          "page.data",
          []
        ],
        [
          "code",
          "{ data, cursor }",
          []
        ],
        [
          "pre",
          "const page = await hal.listRuns();\npage.data.map(r => r.id);\nfor await (const r of hal.runs) {}",
          [
            [
              "code",
              null,
              "const page = await hal.listRuns();\npage.data.map(r => r.id);\nfor await (const r of hal.runs) {}"
            ]
          ]
        ],
        [
          "pre",
          "const runs = await hal.listRuns();\nruns.map(r => r.id);\n// silently capped at 100",
          [
            [
              "code",
              null,
              "const runs = await hal.listRuns();\nruns.map(r => r.id);\n// silently capped at 100"
            ],
            [
              "span",
              "cm",
              "// silently capped at 100"
            ]
          ]
        ],
        [
          "pre",
          "import Halyard from \"halyard\";\nconst hal = new Halyard(\n  process.env.HALYARD_API_KEY\n);",
          [
            [
              "code",
              null,
              "import Halyard from \"halyard\";\nconst hal = new Halyard(\n  process.env.HALYARD_API_KEY\n);"
            ]
          ]
        ],
        [
          "pre",
          "import { Halyard } from \"halyard\";\nconst hal = new Halyard({\n  token: process.env.HALYARD_TOKEN,\n});",
          [
            [
              "code",
              null,
              "import { Halyard } from \"halyard\";\nconst hal = new Halyard({\n  token: process.env.HALYARD_TOKEN,\n});"
            ]
          ]
        ]
      ],
      "authorizedSentenceChange": false
    },
    {
      "slug": "runbook",
      "canonicalSha256": "64f9d4322e821044456be0a4ebd798339a9eb52d1ef16982759eba6995c965c7",
      "sourceTextNodes": 85,
      "numericOccurrences": 41,
      "checks": {
        "allOtherBytesExact": true,
        "cssExact": true,
        "allTextExceptAuthorizedSentence": true,
        "numbers": true,
        "records": true,
        "codeDescendantsExact": true,
        "links": true,
        "noNewRolesOrTabindex": true,
        "noSvgMeterRemoved": true
      },
      "baselineRecords": {
        "preconditions": [
          [
            "You are on the VPN and reach the primary.",
            "psql -h prod-pg-1 -c '\\conninfo'"
          ],
          [
            "The deploy freeze is off.",
            "./bin/freeze status   # => open"
          ],
          [
            "No migration is running.",
            "./bin/migrations running   # => 0"
          ],
          [
            "You can write to the secret store.",
            "vault kv get -field=version \\\n  secret/prod/db"
          ]
        ],
        "steps": [
          [
            "1",
            "Announce it",
            [
              "A rotation nobody expected costs more than the one you announce."
            ],
            "./bin/announce \"rotating api_rw,\n  ~15 min, no downtime expected\"posted to #eng-oncall"
          ],
          [
            "2",
            "Generate the new secret",
            [
              "Written to a staging slot the application does not read yet."
            ],
            "export NEW=$(openssl rand -hex 24)vault kv put secret/prod/db/next \\\n  password=\"$NEW\"Key       Value\nversion   8"
          ],
          [
            "3",
            "Create the successor role",
            [
              "It inherits from the api_rw group role, so you never re-grant table by table."
            ],
            "psql -h prod-pg-1 \\\n  -f sql/rotate.sql \\\n  -v pw=\"$NEW\" -v role=api_rw_q3CREATE ROLE\nGRANT"
          ],
          [
            "4",
            "Point the application at it",
            [
              "This is the step that matters. The sidecar reloads on its own, no restart. Until it returns, the old role is still serving traffic."
            ],
            "vault kv put secret/prod/db \\\n  username=api_rw_q3 \\\n  password=\"$NEW\"./bin/wait-for-rollout \\\n  --key secret/prod/db \\\n  --timeout 120sall 14 pods on version 9 (47s)"
          ],
          [
            "5",
            "Retire the old role",
            [
              "Only once the check below has been clean for five minutes.",
              "If it says objects depend on it: something still owns tables as that role. Stop and escalate \u2014 never REASSIGN OWNED on production alone."
            ],
            "psql -h prod-pg-1 \\\n  -c \"drop role api_rw_q2\"DROP ROLE"
          ]
        ],
        "verify": [
          "Verify \u2014 between steps 4 and 5",
          "Zero on the old role for five straight minutes. That is the bar.",
          "psql -h prod-pg-1 -c \"select usename, count(*) from pg_stat_activity where datname='app' group by 1\" api_rw_q3 | 42 api_rw_q2 | 0",
          "If the old role still has connections: a pod missed the reload. Restart it."
        ],
        "rollback": [
          "Rollback \u2014 safe at any point before step 5 The old secret version and the old role both still exist, so this is a sixty-second undo \u2014 not a deploy. vault kv rollback -version=8 \\ secret/prod/db./bin/wait-for-rollout \\ --key secret/prod/dball 14 pods back on version 8 (39s)"
        ],
        "escalation": [
          "Connections have not drained after 10 minutes. Page /page data-platform and leave both roles in place.",
          "DROP ROLE fails for any reason. Stop and page. Two live roles is safe; half-rotated is not."
        ],
        "facts": [
          "WhenQuarterly",
          "Takes~15 min",
          "DowntimeNone",
          "Needsdb-admin"
        ]
      },
      "canonicalRecords": {
        "preconditions": [
          [
            "You are on the VPN and reach the primary.",
            "psql -h prod-pg-1 -c '\\conninfo'"
          ],
          [
            "The deploy freeze is off.",
            "./bin/freeze status   # => open"
          ],
          [
            "No migration is running.",
            "./bin/migrations running   # => 0"
          ],
          [
            "You can write to the secret store.",
            "vault kv get -field=version \\\n  secret/prod/db"
          ]
        ],
        "steps": [
          [
            "1",
            "Announce it",
            [
              "A rotation nobody expected costs more than the one you announce."
            ],
            "./bin/announce \"rotating api_rw,\n  ~15 min, no downtime expected\"posted to #eng-oncall"
          ],
          [
            "2",
            "Generate the new secret",
            [
              "Written to a staging slot the application does not read yet."
            ],
            "export NEW=$(openssl rand -hex 24)vault kv put secret/prod/db/next \\\n  password=\"$NEW\"Key       Value\nversion   8"
          ],
          [
            "3",
            "Create the successor role",
            [
              "It inherits from the api_rw group role, so you never re-grant table by table."
            ],
            "psql -h prod-pg-1 \\\n  -f sql/rotate.sql \\\n  -v pw=\"$NEW\" -v role=api_rw_q3CREATE ROLE\nGRANT"
          ],
          [
            "4",
            "Point the application at it",
            [
              "This is the step that matters. The sidecar reloads on its own, no restart. Until it returns, the old role is still serving traffic."
            ],
            "vault kv put secret/prod/db \\\n  username=api_rw_q3 \\\n  password=\"$NEW\"./bin/wait-for-rollout \\\n  --key secret/prod/db \\\n  --timeout 120sall 14 pods on version 9 (47s)"
          ],
          [
            "5",
            "Retire the old role",
            [
              "Only once the verification check has been clean for five minutes.",
              "If it says objects depend on it: something still owns tables as that role. Stop and escalate \u2014 never REASSIGN OWNED on production alone."
            ],
            "psql -h prod-pg-1 \\\n  -c \"drop role api_rw_q2\"DROP ROLE"
          ]
        ],
        "verify": [
          "Verify \u2014 between steps 4 and 5",
          "Zero on the old role for five straight minutes. That is the bar.",
          "psql -h prod-pg-1 -c \"select usename, count(*) from pg_stat_activity where datname='app' group by 1\" api_rw_q3 | 42 api_rw_q2 | 0",
          "If the old role still has connections: a pod missed the reload. Restart it."
        ],
        "rollback": [
          "Rollback \u2014 safe at any point before step 5 The old secret version and the old role both still exist, so this is a sixty-second undo \u2014 not a deploy. vault kv rollback -version=8 \\ secret/prod/db./bin/wait-for-rollout \\ --key secret/prod/dball 14 pods back on version 8 (39s)"
        ],
        "escalation": [
          "Connections have not drained after 10 minutes. Page /page data-platform and leave both roles in place.",
          "DROP ROLE fails for any reason. Stop and page. Two live roles is safe; half-rotated is not."
        ],
        "facts": [
          "WhenQuarterly",
          "Takes~15 min",
          "DowntimeNone",
          "Needsdb-admin"
        ]
      },
      "codeInventory": [
        [
          "code",
          "./bin/freeze status   # => open",
          []
        ],
        [
          "code",
          "./bin/migrations running   # => 0",
          []
        ],
        [
          "code",
          "/page data-platform",
          []
        ],
        [
          "code",
          "DROP ROLE",
          []
        ],
        [
          "code",
          "REASSIGN OWNED",
          []
        ],
        [
          "code",
          "api_rw",
          []
        ],
        [
          "code",
          "api_rw",
          []
        ],
        [
          "code",
          "prod-pg-1",
          []
        ],
        [
          "code",
          "psql -h prod-pg-1 -c '\\conninfo'",
          []
        ],
        [
          "code",
          "vault kv get -field=version \\\n  secret/prod/db",
          []
        ],
        [
          "pre",
          "./bin/announce \"rotating api_rw,\n  ~15 min, no downtime expected\"posted to #eng-oncall",
          [
            [
              "b",
              null,
              "./bin/announce \"rotating api_rw,\n  ~15 min, no downtime expected\""
            ],
            [
              "span",
              null,
              "posted to #eng-oncall"
            ]
          ]
        ],
        [
          "pre",
          "export NEW=$(openssl rand -hex 24)vault kv put secret/prod/db/next \\\n  password=\"$NEW\"Key       Value\nversion   8",
          [
            [
              "b",
              null,
              "export NEW=$(openssl rand -hex 24)"
            ],
            [
              "b",
              null,
              "vault kv put secret/prod/db/next \\\n  password=\"$NEW\""
            ],
            [
              "span",
              null,
              "Key       Value\nversion   8"
            ]
          ]
        ],
        [
          "pre",
          "psql -h prod-pg-1 -c \"select usename,\n  count(*) from pg_stat_activity\n  where datname='app' group by 1\" api_rw_q3 | 42\n api_rw_q2 |  0",
          [
            [
              "b",
              null,
              "psql -h prod-pg-1 -c \"select usename,\n  count(*) from pg_stat_activity\n  where datname='app' group by 1\""
            ],
            [
              "span",
              null,
              " api_rw_q3 | 42\n api_rw_q2 |  0"
            ]
          ]
        ],
        [
          "pre",
          "psql -h prod-pg-1 \\\n  -c \"drop role api_rw_q2\"DROP ROLE",
          [
            [
              "b",
              null,
              "psql -h prod-pg-1 \\\n  -c \"drop role api_rw_q2\""
            ],
            [
              "span",
              null,
              "DROP ROLE"
            ]
          ]
        ],
        [
          "pre",
          "psql -h prod-pg-1 \\\n  -f sql/rotate.sql \\\n  -v pw=\"$NEW\" -v role=api_rw_q3CREATE ROLE\nGRANT",
          [
            [
              "b",
              null,
              "psql -h prod-pg-1 \\\n  -f sql/rotate.sql \\\n  -v pw=\"$NEW\" -v role=api_rw_q3"
            ],
            [
              "span",
              null,
              "CREATE ROLE\nGRANT"
            ]
          ]
        ],
        [
          "pre",
          "vault kv put secret/prod/db \\\n  username=api_rw_q3 \\\n  password=\"$NEW\"./bin/wait-for-rollout \\\n  --key secret/prod/db \\\n  --timeout 120sall 14 pods on version 9 (47s)",
          [
            [
              "b",
              null,
              "vault kv put secret/prod/db \\\n  username=api_rw_q3 \\\n  password=\"$NEW\""
            ],
            [
              "b",
              null,
              "./bin/wait-for-rollout \\\n  --key secret/prod/db \\\n  --timeout 120s"
            ],
            [
              "span",
              null,
              "all 14 pods on version 9 (47s)"
            ]
          ]
        ],
        [
          "pre",
          "vault kv rollback -version=8 \\\n  secret/prod/db./bin/wait-for-rollout \\\n  --key secret/prod/dball 14 pods back on version 8 (39s)",
          [
            [
              "b",
              null,
              "vault kv rollback -version=8 \\\n  secret/prod/db"
            ],
            [
              "b",
              null,
              "./bin/wait-for-rollout \\\n  --key secret/prod/db"
            ],
            [
              "span",
              "hl",
              "all 14 pods back on version 8 (39s)"
            ]
          ]
        ]
      ],
      "authorizedSentenceChange": true
    }
  ],
  "textNodes": 262,
  "numericOccurrences": 147,
  "pass": true,
  "originalKeyedGroups": 49,
  "note": "Same author record parser reused; independent byte-level allowlist plus original code/descendant comparison. Only Runbook one sentence adjusted in expected records; actual source remains explicit."
}
