Skip to content

feat(ops-mission-control): add an incident.io provider - #4951

Open
stephen-meehan-lrn wants to merge 1 commit into
kirodotdev:mainfrom
stephen-meehan-lrn:feat/ops-incidentio
Open

feat(ops-mission-control): add an incident.io provider#4951
stephen-meehan-lrn wants to merge 1 commit into
kirodotdev:mainfrom
stephen-meehan-lrn:feat/ops-incidentio

Conversation

@stephen-meehan-lrn

@stephen-meehan-lrn stephen-meehan-lrn commented Aug 21, 2026

Copy link
Copy Markdown

Problem / Motivation

Ops Mission Control can ingest signals from CloudWatch, Datadog, GitHub Issues and a
generic webhook, but not from incident.io. An operator whose alerting already lives in
incident.io has no way to get those alerts onto the board: they cannot be claimed,
investigated, fingerprinted against the knowledge ledger, or resolved through the app.
The on-call schedule is equally invisible, so the rotation tier has nothing to consult
for an incident.io-based team.

Why it matters

Without a provider, an incident.io shop gets no value from the board at all — the
signals it needs to triage are exactly the ones the app cannot see. Adding it also
exercises the provider seam a second time for a rotation-capable source, which is where
the authorization boundary lives.

What changed (motivation → approach → change)

Goal: make incident.io a first-class source — signals, rotation and actions —
without weakening the existing authorization model.

Approach, and why this shape:

  • Alerts, not incidents, are the signal. An incident.io alert carries a strict
    firing/resolved status, which is the same shape as a signal's own lifecycle, so
    absence from a poll is meaningful. A declared incident is a human artefact with an
    eight-category status whose transitions run a post-incident flow; putting one on the
    board would duplicate work a responder already owns.
  • Only resolve and comment are offered. The API has no acknowledged state and
    no snooze/mute/suppress for a single alert (a maintenance window is account-level
    config). Advertising ack or silence would pass the autonomy gate and then fail at
    execute time, after the board had recorded the action as granted.
  • The rotation identity is keystone-fenced, not a provider config field.
    incident.io has no self-identity endpoint — an API key resolves to the key, not a
    person — so the operator's own user id cannot be derived at runtime. Provider config
    is agent-writable, so an identity living there could be forged to claim the shift and
    authorize a write it does not own. It sits in policy_store.OPERATOR_ONLY_KEYS as
    incidentio.user_id, written only by the authenticated PUT /settings, exactly like
    pagerduty.user_id.
  • A source with no identity abstains (ShiftStatus(on_shift=True, unknown=True)),
    matching the documented unknown contract in providers/base.py and PagerDuty's own
    abstention branches. An empty agent-writable schedule_ids with an operator
    identity present votes off shift instead, so a config write cannot manufacture the
    abstention the tier gate treats as a non-vote.
  • The poll walks the cursor, and the truncation verdict is derived from the slice.
    The alerts endpoint caps a page at 50, well under the registry's 100-signal cap, so
    stopping at one page made any estate over 50 firing alerts report a truncated poll on
    every cycle — permanently non-authoritative, so reconcile could never resolve one
    of this source's signals. The walk now pages until the estate is exhausted, and the
    verdict is computed from whether the final slice actually discarded anything rather
    than from whichever branch ended the loop. That last part is deliberate: three
    separate review findings in this loop were all the same defect — a branch ending the
    walk while the verdict disagreed with what the slice dropped — so deriving the two
    from one fact makes "dropped an alert but called the poll complete" unrepresentable.

Also: client-side filtering of alert_source_ids, because the endpoint documents an
alert_source[one_of] filter but not how to encode multiple values, and a guessed
encoding that matches nothing presents as a quiet estate.

Tests

tests/test_providers_incidentio.py (15 tests):

  • Rotation identity (4). A blank identity abstains and does not even query; an
    empty agent-writable schedule_ids with an operator identity present votes off shift
    rather than abstaining; a genuinely unconfigured provider abstains and fails open; the
    effective schedule (final) decides rather than the pre-override rotation rules, so
    a covered shift is not read as ours.
  • Unsupported verbs (2). supported_actions() is exactly {resolve, comment}, and
    ack/silence are refused without the provider ever being called.
  • Poll completeness (7). A cursor is followed (asserting the second request is
    made and carries after) rather than reported as truncation; an estate past the cap
    reports truncated; a terminal page that overshoots the cap still reports truncated
    (the regression guard for a dropped-but-called-complete poll); an estate of exactly
    the cap is not truncated; an empty page carrying a cursor is truncation, while an
    empty terminal page without one stays authoritative; client-side source filtering
    preserves the verdict.
  • Signal mapping (2). The deduplication key is the exact-match provider_key when
    present, and the alert id is carried in labels for the action path.

Three existing tests in tests/test_providers.py were extended to assert the adapter
registers and reports unconfigured rather than raising.

Manual verification

Run against a real incident.io account in an isolated pod (own KIROCREW_HOME, no
crons), not the live gateway. The provider polled 50 firing alerts and the Signals
tab showed the source ok. That run is what surfaced the one-page truncation defect
above — 50 firing alerts is exactly one page, so the source was permanently
non-authoritative and no unit test had reason to notice.

The truncation fixes were then proven rather than assumed: reverting the production
hunk while keeping the tests makes
test_a_terminal_page_that_overshoots_the_cap_reports_truncated fail (1 failed / 14
passed), and restoring it returns 15 passed.

Local gate floor green on this commit: pytest (926 passed / 44 skipped in the app
suite), black, isort, flake8, mypy, tsc -b, eslint, vitest, and i18n:check at
18/18 — that last one initially failed and is what forced real translations rather than
English placeholders in the non-English catalogs.

Screenshots / video

The provider card, showing the new keystone-fenced identity field, its help text, and
the roles the adapter registers:

incident.io provider card with the operator identity field

The Signals tab with the source live against a real account:

Signals tab showing incident.io ok with 50 firing alerts

Related Issues

no linked issue: this adds a provider that was not tracked by an existing issue.

Checklist

  • Single commit with a Conventional Commits title (feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (if applicable)
  • No secrets, credentials, or internal references in the diff

@stephen-meehan-lrn
stephen-meehan-lrn requested a review from a team August 21, 2026 15:54
@stephen-meehan-lrn
stephen-meehan-lrn requested a review from a team as a code owner August 21, 2026 15:54
@github-actions github-actions Bot added fork Pull request from a fork (external contributor) readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 21, 2026
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Aug 21, 2026
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5, fork) — 🟡 CONCERNS

Design-level review of f8971e96575820d4fdd92462a84766cd4fafdf16 via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

I have everything I need. The adapter is a faithful second instantiation of the provider seam — it mirrors PagerDuty's keystone-fenced identity, abstention asymmetry, and the cap+1 truncation contract, and the registry's 15s wait_for bounds the new multi-page walk. Two residuals survive the kill-filter: the write verbs were never exercised against the real API, and the spec's per-poller truncation enumeration wasn't extended.

Design-Verdict: CONCERNS

Sound, pattern-faithful provider addition; the two write verbs ship unexercised against the real API, and the spec's poller enumeration was not extended.

Watch

  • The manual run verified polling only ("The provider polled 50 firing alerts…"); /v1/alert_notes and /v2/alerts/{id}/actions/resolve were never executed against a real account. A wrong endpoint or body shape presents as every action failing at execute time — the exact granted-then-fails failure the PR's own verb-narrowing rationale exists to prevent. Failure is graceful (ActionResult ok=False) and reversible, hence CONCERNS not BLOCK.
    Clears when: the author confirms resolve and comment were executed against a real alert, or cites the vendor API reference for both endpoint shapes.
  • The spec's truncation section ("one class across five pollers… All four return TruncatedSignals", ops-mission-control.md:2055–2072) enumerates each adapter's detector; incidentio adds a sixth poller with a novel detector (cursor walk, verdict derived from the slice) and no bullet, and "Both identities are reported back on GET /rotation" (line 1311) is now three. The module contract under-documents exactly the mechanism this PR argues hardest for.
    Clears when: the same commit extends the truncation enumeration and the identities sentence to cover incidentio.

Suggestions

  • Add an explicit page ceiling to the cursor walk, mirroring CloudWatch's documented _MAX_ALARM_PAGES precedent — a page of entries that are all non-dict advances neither alerts nor the empty-page break, so only the registry timeout ends the walk while the orphaned thread keeps requesting.

[DESIGN-REVIEWED] f8971e9

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review (fork) — 🔴 changes requested (blocking)

Reviewed f8971e96575820d4fdd92462a84766cd4fafdf16 via the fork AI-review pipeline; updated in place on each push.

1 of 1 blocking finding(s) are security-class and were withheld from adjudication, so the blocking verdict stands.

BLOCKING -- src/kiro_crew/apps/builtins/ops_mission_control/backend/routes.py:2142 -- Keystone write failures crash settings saves
await asyncio.to_thread(policy_store.put, policy_store.INCIDENTIO_USER_KEY, inc_user)
Corrupt or unwritable policy store -> settings PUT reaches this unguarded write after earlier writes -> 500 with partially applied settings.
Anchor: residual/crash-data-loss-corruption
Fix: Route the write through _settings_write_or_refuse.

[BLOCK-MERGE] f8971e9
[GPT-REVIEWED] f8971e9

Adjudication (Opus 4.8) — is blocking on each finding proportionate?

I have enough to rule. F1 targets the changed line at routes.py:2142 (await asyncio.to_thread(policy_store.put, policy_store.INCIDENTIO_USER_KEY, inc_user)), which bypasses _settings_write_or_refuse that every sibling keystone write in _handle_put_settings uses.

Fenced record for F1:

  • Condition — unguarded write: authentic.patch line 498 calls policy_store.put directly, while the four siblings (routes.py:1991, 2002, 2015, 2030, 2119) all route through _settings_write_or_refuse, which catches OSError→503 and JSONDecodeError→corrupt-refusal (routes.py:1691-1714).

  • Reachability is ordinary, not extreme: a request carrying only incidentio_user_id reaches the unguarded write with no earlier guarded write intervening; the corrupt/unwritable policy store is precisely the condition the whole helper exists for (routes.py:1663-1669). The INCIDENTIO_USER_KEY is an operator-only rotation-identity fence feeding the off-shift refusal, so the "did my change land?" ambiguity is the same security-relevant one the guard was built to close.

  • Recovery: none in-band — the escape becomes aiohttp's default 500 with no code for the UI to branch on, unlike the sibling 503/code path.

  • The conditions are neither mutually contradicting nor impossible to produce, so a human could not treat the residual risk as extreme. Not FLAG-eligible.

    [ADJUDICATION] f8971e9 total=0 uphold=0 downgrade=0
    [GPT-ADJUDICATED] f8971e9
    [ADJUDICATION-FENCED] f8971e9 fenced=1 flagged=0
    UPHOLD-FENCED F1 src/kiro_crew/apps/builtins/ops_mission_control/backend/routes.py:2142 -- An incidentio-only settings PUT reaches this unguarded keystone write on an ordinary corrupt/unwritable store, yielding a bare 500 with no code and a possible partial ceiling apply — the exact ambiguity every sibling write's guard exists to close.
    [GPT-ADJUDICATED-FENCED] f8971e9

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5, fork) — 🟡 CONCERNS

Premise-level review of f8971e96575820d4fdd92462a84766cd4fafdf16 via the fork AI-review pipeline — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

First-Principles-Verdict: CONCERNS

The new keystone write bypasses _settings_write_or_refuse — the one write in _handle_put_settings not using the helper built exactly for it.

Not justified as shipped

  • Item 4 — duplicate of _settings_write_or_refuse (routes.py:1654): the key itself is derived (agent-forgeable on-call identity is a named boundary), but its write is a bare asyncio.to_thread(policy_store.put, ...) while the 10 sibling settings writes — including the adjacent pd_user this key claims to be "exactly like" — go through the helper, whose docstring says a refused keystone write must not surface as a code-less 500. A second spelling that has already diverged.
  • Item 10 — rides along, undeclared: two PNGs under temp-screenshots/incidentio-provider/; matches the repo's committed-deliverables convention (344 sibling dirs, named in .gitignore), so inventory only.

What this change ships

Intent: let a team whose alerting lives in incident.io triage, claim and resolve those alerts from the Ops Mission Control board — an ADDITION.

  1. incident.io firing alerts appear as board signals, paged to the poll cap — justified
  2. incident.io schedules vote in the on-shift decision, using the effective (final) rota — justified
  3. Alerts can be resolved or commented from the board; ack/silence never offered — justified
  4. New operator-only dashboard setting "Your incident.io user ID" (keystone key + PUT /settings field) — duplicate of _settings_write_or_refuse (routes.py:1654), write path only
  5. Agent-writable alert_source_ids / schedule_ids config keys scope what is polled — justified
  6. An empty schedule list with an operator identity set now votes off-shift instead of abstaining — justified
  7. Spec gains the "a sink covers only the verbs its provider has" paragraph — justified
  8. Identity label and help text in 13 locale catalogs — justified
  9. Rotation status now reports the incidentio identity to the settings panel — justified
  10. Two provider screenshots land in temp-screenshots/incidentio-provider/ — rides along, undeclared

Watch

  • The verb set and the spec paragraph rest on an unverified provider claim: "the API has no snooze, mute or suppress call for a single alert". The manual run exercised polling, not endpoint absence; under-offering is the safe direction, but the spec now asserts it as fact. Clears when: confirmed against incident.io's API reference or the live account already used for verification.
  • The bypassed helper above. Clears when: the inc_user write goes through _settings_write_or_refuse like its 10 siblings.

Subtractions

  • Replace the bare asyncio.to_thread(policy_store.put, policy_store.INCIDENTIO_USER_KEY, ...) in routes.py with the existing _settings_write_or_refuse(policy_store.put, ..., code="policy_store_unwritable", applied=applied) — 10 call sites counted for the helper; this is the lone keystone write outside it.
  • Drop truncated = True inside the cap branch of _poll_sync (incidentio.py) — the post-loop derivation runs the same len(alerts) > DEFAULT_POLL_LIMIT comparison and already sets it; keep only the break.

[FIRST-PRINCIPLES-REVIEWED] f8971e9

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5, fork) — 🟡 CONCERNS

UX-level review of f8971e96575820d4fdd92462a84766cd4fafdf16 via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

UX-Verdict: CONCERNS

A pattern-faithful clone of the PagerDuty provider row, but its screenshots live only on the fork — no first-time reader has seen any of it.

Watch

  • Adjacent cards describe the same blank-identity behavior in two vocabularies: PagerDuty's help says "leave blank to treat anyone on the schedule as you" while the new incidentio_user_id_help says "leave it blank and incident.io abstains from the rotation vote" — both code paths return the identical ShiftStatus(on_shift=True, unknown=True). An operator reading both rows concludes the providers handle a blank differently, and "rotation vote" is internal mechanism vocabulary (it names the tier-gate ballot, not anything visible on screen). Every configuring operator reads it, misunderstanding not failure, persistent → CONCERNS. Fix: reword the new string's last clause in consequence language mirroring the sibling.

Evidence gaps

  • The incident.io provider row (enable toggle, Ready/Not set up badge, detail line, alert_source_ids / schedule_ids fields, api_key secret) — temp-screenshots/incidentio-provider/providers-list.png is an added binary in the patch, not on disk; push the branch to this repo to materialize it for the blind read.
  • The "Your incident.io user ID" field, its help text, and Save button — same for identity-field.png.
  • No blind read ran in this lane, so first-time comprehension of "Your incident.io user ID" and the help copy is unestablished.

[UX-REVIEWED] f8971e9

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review (fork) — ✅ no blocking findings

Reviewed f8971e96575820d4fdd92462a84766cd4fafdf16 via the fork AI-review pipeline; updated in place on each push.

Review details

I've verified the candidate against the code. The repo on disk is at the base commit, but the diff plus policy_store.py/routes.py confirm every prong:

  • _settings_write_or_refuse (routes.py:1654) wraps every other keystone write and maps OSError→503-with-code and json.JSONDecodeError→corruption refusal, plus writes an _audit(..., "failure") line.
  • policy_store.put_read_for_update() (policy_store.py:388, 174) raises OSError on a transient read failure and CorruptDocumentError on a corrupt/non-object/non-UTF-8 file; CorruptDocumentError subclasses json.JSONDecodeError (models.py:71), so the helper catches both.
  • The added inc_user block calls policy_store.put bare, so those exceptions escape _handle_put_settings unhandled.

The consequence is an error-response inconsistency plus a skipped failure-audit line — not data loss, corruption, privilege escalation, credential exposure, or a process crash (the write is abandoned before touching the file, failing closed). The minimal fix is in-diff. That places it as an advisory FINDING, not BLOCKING.

One advisory finding: the incident.io keystone write skips the shared refusal helper every sibling write uses.

FINDING — src/kiro_crew/apps/builtins/ops_mission_control/backend/routes.py:2142 — the added await asyncio.to_thread(policy_store.put, policy_store.INCIDENTIO_USER_KEY, inc_user) bypasses _settings_write_or_refuse, so a transient OSError or a CorruptDocumentError from _read_for_update escapes to aiohttp as a bare 500 (no code, plain body) and skips the _audit("settings_put", ..., "failure") line every other keystone write records on a refusal → Fix: route it through _settings_write_or_refuse(policy_store.put, policy_store.INCIDENTIO_USER_KEY, inc_user, code="policy_store_unwritable", applied=applied) and return refused on non-None, mirroring the pd_user block directly above.

[OPUS-REVIEWED] f8971e9

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 21, 2026
@stephen-meehan-lrn

Copy link
Copy Markdown
Author

Both blocking findings land in incidentio.py, and they get different answers. Fixed in b92bce94a933c97ef010193c6a9e83b0e29880b6.

  • Future shifts authorize actions before handoff (incidentio.py:265) — fixed.

Confirmed against the code. entry_window_start/end selects schedule entries that OVERLAP the window rather than ones containing it, and the match loop filtered only on user.id, never on whether the entry was in force. With _SHIFT_WINDOW = timedelta(minutes=1) the concrete exposure is up to 60 seconds of early authorization: an entry starting at now + 30s was returned and read as on_shift=True while the outgoing engineer still held the page. The magnitude is small but the mechanism is exactly as described, and it scales with a constant a later change could widen.

The fix does not narrow the window, because the window is only there to satisfy the endpoint's required range parameter. Instead the matched entry must now contain the instant: a new _covers(entry, now) requires start_at <= now < end_at, so the window's width decides how far ahead we LOOK and never who is judged on call. A missing or unparseable bound returns False rather than True: this value feeds an authorization gate, so an entry that cannot be evaluated does not grant the shift.

Four tests pin it (TestIncidentIoAuthorizesOnlyTheShiftInForce): a shift starting in 30s does not authorize, the shift containing now still does, and each of an absent start_at and an unparseable end_at fails closed. Proven rather than assumed - reverting only the _covers call makes 3 of the 4 fail and leaves the control passing. App suite 933 passed / 44 skipped.

  • Agent-writable schedule selection bypasses the rotation fence (incidentio.py:98) — needs-a-decision, and the question is for the maintainer.

The concern holds in the abstract and I am not disputing the mechanism: on-shift is a function of BOTH the identity and the schedule list, the identity is keystone-fenced (policy_store.INCIDENTIO_USER_KEY, operator-only), and schedule_ids is in config_fields and therefore writable through PUT /provider/<id>/config. So the constrained party cannot change who it claims to be, but it can change which rotation is consulted, and if the operator is genuinely on call on some other schedule that write yields on_shift=True for a rotation the operator never nominated for automation.

What stops me acting on it unilaterally is that this is the established core shape, not something this PR introduces. The reference adapter has it identically: pagerduty.py:103 lists schedule_ids in config_fields, and its own comment at pagerduty.py:186 states outright that schedule_ids lives in agent-writable config.json. This adapter mirrors that split deliberately, including the empty-list-with-identity vote that exists so a config write cannot manufacture an abstention.

So fencing schedule_ids here would diverge from the convention every other rotation provider follows AND leave the identical exposure in PagerDuty, which means it would not actually close the hole it names. Whether the rotation fence should cover schedule selection as well as identity is a contract change across all rotation providers, and that is a maintainer's call rather than mine to make inside a new-provider PR. Deliberately not filing an issue for it, because what is missing is the decision, not a task someone could pick up.

If the ruling is that the fence should cover it, the shape is a new operator-only keystone key holding the schedule list for every rotation provider, with schedule_ids removed from both adapters' config_fields - happy to do that here or in a follow-up, whichever you prefer.

@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 21, 2026
@bolichen97
bolichen97 enabled auto-merge (squash) August 24, 2026 06:57
auto-merge was automatically disabled August 25, 2026 09:19

Head branch was pushed to by a user without write access

@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Aug 25, 2026
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 25, 2026
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 25, 2026
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 25, 2026
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 26, 2026
@stephen-meehan-lrn

Copy link
Copy Markdown
Author

Request: re-run of a failed backend shard (fork PR, no rerun permission)

Could a maintainer please re-run the failed jobs on run 32953993715, or approve the pending
fork-PR workflow runs on this head? I cannot do either myself: gh run rerun returns
Must have admin rights to Repository, and the newly triggered runs on this head report
action_required.

Current head: 1f44e37b849c38e0efd82042e3362174bec8f186 (one commit on cf193df5).

The single failing job is Backend Tests (3.10, 4), and it fails on two tests that this PR
does not touch:

FAILED test/test_windows_gap_list.py::test_no_entry_is_a_test_that_only_runs_on_windows - Failed: Timeout >120.0s
FAILED test/test_windows_gap_list.py::test_every_entry_names_a_test_that_still_exists - Failed: Timeout >120.0s

Why I believe this is runner flakiness rather than a defect in the branch:

  • Both tests pass locally in 3.62s for the whole file (5 tests). A 3.6s test reporting
    Timeout >120.0s points at a starved runner, not at the assertions.
  • This PR modifies neither test/test_windows_gap_list.py nor test/windows-expected-failures.txt,
    and the failing shard's log contains no reference to the paths this PR adds.
  • The job ran 09:39:56Z to 09:52:18Z, about 12 minutes, so it was not near the job time limit.
  • Coverage Gate fails closed with backend-test=failure -- failing closed., and PR Readiness
    follows from that. Both are cascades of the one job, not separate findings.

For context on why I am asking instead of pushing again: the branch has been rebased and re-pushed
three times, and each round failed on a different non-deterministic timeout in code the PR does
not touch.

Round Job Symptom
1 Backend Tests (3.12, 1) exceeded the maximum execution time of 30m0s
2 Backend Tests (Windows) (4) TimeoutError in test/test_webhooks_api.py::TestOneTurnPerSessionKey::test_concurrent_same_key_is_claimed_before_capacity_await
3 Backend Tests (3.10, 4) the two 120s timeouts above

Round 2's Windows shard passed on round 3 with byte-identical application code, which is what
convinced me these are infrastructure flakes. Rebasing a fourth time would only re-enter the same
lottery, so I have stopped doing that.

Everything else is green: 37 of 40 jobs pass, and locally the app suite is 939 passed / 44 skipped,
with black, isort, flake8, mypy, tsc -b, eslint, i18n:check (19/19) and the frontend suite
(1552 files / 24297 tests) all clean.

Happy to rebase onto current main instead if you would prefer a fresh run to a re-run.

@bolichen97

Copy link
Copy Markdown
Collaborator

Open PR relationship audit

This is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion.

Relationship findings

  • PR #8084 is OVERLAPPING relative to this PR. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #8084: REBASE. Complementary work in the same two modules with different goals; neither blocks the other. Files: src/kiro_crew/apps/builtins/ops_mission_control/backend/policy_store.py, src/kiro_crew/apps/builtins/ops_mission_control/backend/rotation.py.

No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit.

Registers `incidentio` as a public adapter implementing SignalSource,
RotationSource and ActionSink over the incident.io REST API.

Built on alerts rather than incidents: an alert's status is a strict
firing/resolved enum, which is the same shape as a signal's own lifecycle, so
absence from a poll means it cleared. A declared incident is a human artefact
whose transitions run a post-incident flow, and putting one on the board would
duplicate work a responder already owns.

The sink offers `resolve` and `comment` only. The API has no acknowledged state
and no snooze, mute or suppress call for a single alert, and advertising a verb
the provider cannot perform would pass the autonomy gate and then fail at
execute time, after the board had recorded the action as granted.

Truncation is detected from the response cursor, not by fetching one item past
the cap: 50 is the endpoint's maximum page size, so a larger request is clamped
and reads back as a complete page. Alert-source filtering runs client-side
because the documented filter's multi-value encoding is not specified, and a
guessed encoding that matches nothing presents as a quiet estate; the
truncation verdict is therefore taken from the raw response and survives
filtering.

The rotation identity lives on the keystone (`incidentio.user_id`), not in
provider config. incident.io has no self-identity endpoint, so it cannot be
derived at runtime, and provider config is agent-writable — a forged identity
there would let the constrained party claim the shift and authorize a write it
does not own. A source with no identity abstains rather than voting off shift,
and an empty (agent-writable) schedule list with an operator identity present
votes off shift rather than manufacturing an abstention.
@bolichen97

Copy link
Copy Markdown
Collaborator

Rebased onto main 41dcadf2c by a maintainer as part of the 2026-09-08 open-PR audit. Branch head is now f8971e965 (was 1f44e37b8).

Clean rebase: no conflicts, no content changes to your commit.

Gates run locally on the changed files only: black, isort, flake8 (routes.py is already listed in .github/black-baseline.txt and fails black --check identically on main and on your old head, so that is pre-existing, not from the rebase); pytest on test_providers_incidentio.py, test_providers.py, test_config_routes.py (193 passed); tsc --noEmit on the website project (clean).

Please review the rebase. A maintainer push makes the maintainer the last pusher, so under this repo's last-push rule a second approver is needed. Reply here if anything looks wrong.

@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fork Pull request from a fork (external contributor) readiness: action required A blocking check or review needs attention

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants