Skip to content

triage-propose

The coxswain dispatches triage-propose when a new ticket arrives unscoped: it reads the ticket and proposes which initiative it belongs to, or that it needs a new one. What lands is a filing proposal in the future-work landing area, not a ticket placed directly onto the active board.

triage-propose

Alerts arrive as an argument rather than being fetched here. A graph that reads a queue reads the world, and the contract puts both the filesystem and the clock on the far side of the graph boundary for the same reason: a graph that cannot be replayed cannot be debugged after the fact.

flowchart LR
    n0["fetch<br/>step · 1"]
    n1["classify<br/>step · 2"]
    n2["verify<br/>step · 3"]
    n3["emit<br/>step · 4"]
    n0 --> n1
    n1 --> n2
    n2 --> n3
    style n0 fill:#f96,stroke:#333,stroke-width:2px
Node Step
fetch 1
classify 2
verify 3
emit 4

triage-propose — specification

Morning triage of an alert queue. Strictly read-only everywhere.

Node Role Tier Notes
fetch standard open alerts from the queue; capped, overflow counted not dropped
classify triage_classify cheap symptom key per alert, against a runbook index
verify evidence_verify deep runbook-guided; deterministic checks run verbatim
emit proposals with evidence attached

Args: run_id, date, cartridge (required, no fallback), optional max_alerts (default 15).

The sequence is the least interesting thing here. What matters is the two caps and the fact that neither one drops anything on the floor:

flowchart TB
    QUEUE["alerts arrive as an argument<br/>the graph never reads the queue"]
    QUEUE --> FETCH

    FETCH{"fetch<br/>cap: max_alerts, 15"}
    FETCH -- "over the cap" --> OVER["deferred_overflow<br/>counted, returns next run"]
    FETCH -- "within the cap" --> CLASSIFY

    CLASSIFY["classify<br/>role: triage_classify, cheap<br/>against the runbook index"]
    CLASSIFY --> VCAP{"cap: verify_cap, 5"}
    VCAP -- "over the cap" --> CAPD["deferred_for_capacity<br/>classified, not verified"]
    VCAP -- "within the cap" --> VERIFY

    VERIFY["verify<br/>role: evidence_verify, deep<br/>runbook checks run verbatim<br/>plus the trap for this symptom"]

    VERIFY -- "actionable, with checks" --> EMIT["emit<br/>proposals and evidence"]
    VERIFY -- "not actionable" --> QUIET["no proposal"]
    VERIFY -- "no entry matched, trap did not hold,<br/>weak match, or explicit correction" --> HEAL["doc_update proposal<br/>the runbook heals itself"]

    HEAL -. "amends the entry classify used" .-> CLASSIFY

    OVER --> TOTALS
    CAPD --> TOTALS
    EMIT --> TOTALS
    QUIET --> TOTALS
    HEAL --> TOTALS

    TOTALS[["totals: received, fetched, verified,<br/>deferred_overflow, deferred_for_capacity, runbook_gaps"]]

Every path ends at totals. That is deliberate: a graph that drops nine of ten alerts and reports success on the tenth is worse than one that fails, so both caps report what they set aside rather than truncating silently. Note also that only a verified alert can produce a proposal — an unverified one has no evidence, and a claim without evidence is not a proposal.

Runbook index is a cartridge-provided path, not a skill-layout assumption. Each entry carries match, the kinds it may emit, a risk, and — most usefully — the trap: the known wrong belief for that symptom. A runbook that only states the right answer lets the next person re-derive the wrong one.

The runbook heals itself

The index is not read-only input. A run is the only moment anyone has the evidence to improve an entry, and by the time the incident is over nobody goes back, so verify reports two things the runbook cannot learn any other way — whether the entry's trap actually held, and what the entry gets wrong. Four cases emit a doc_update proposal:

Case Proposal
No entry matched the symptom add an entry, with its trap
The node named an explicit correction amend that entry, verbatim
The stated trap did not hold amend the trap — as written it points at the wrong belief
The match was weak (confidence: low) sharpen the match criteria

This does not put a write into a read-only graph: a doc_update is a proposal like any other, gated like any other. It is deferred in the base taxonomy, so it cannot auto-apply until the eligible kinds have earned their ramp — the runbook does not get to start rewriting itself on day one.

That ramp is currently measured per kind, which averages every entry in the index together. It should be measured per entry, with incidents demoting the entry they implicate — see docs/RUNBOOK-TRUST.md.

Fetch cap must exceed the verify cap comfortably; a busy queue otherwise blows the structured-output limit and the run dies mid-flight. Overflow defers to the next run and is reported in the totals.

Status: implemented in triage_propose.py. Alerts arrive as an argument — the graph does not read the queue itself, because a node that fetches cannot be replayed. Both caps report their overflow in totals.