Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions kora_cli/alerts/wake_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,15 +702,29 @@ def format_operator_dm(
def format_fallback_text(
event_details: Dict[str, Any], *, reason: str
) -> str:
"""When reasoning fails, send the alert details verbatim + the
failure reason. Operator still gets actionable signal."""
"""When reasoning fails, send the alert details verbatim + a
clear "review and act manually" footer. Operator still gets
actionable signal — the alert itself (category + severity +
alert_id) is visible even when reasoning can't run.

KR-CC1-POLISH (#198): mirrors the probe wake consumer's
fallback shape (#184) — header line with alert identity, then
a footer line that surfaces (a) the engine's failure reason
and (b) explicit "act manually" guidance so the operator
isn't left wondering whether Kora is going to retry.
"""
category = event_details.get("category") or "unknown"
severity = event_details.get("severity") or "warning"
alert_id = event_details.get("alert_id") or "unknown"
channel = event_details.get("channel") or "unknown"
return (
f"{category} ({severity}): alert id {alert_id}\n"
f"{category} ({severity}): alert id {alert_id} "
f"(via {channel})\n"
f"\n"
f"I was unable to investigate — engine returned: {reason}"
f"Kora is unavailable to investigate this alert "
f"(engine returned: {reason}). Review the alerts panel "
f"and act manually — Kora will not retry this "
f"investigation."
)


Expand Down
20 changes: 20 additions & 0 deletions kora_cli/audit/jsonl_sink.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,26 @@
# mirrors probe.wake_requested for alert investigations. Reads return [] until
# the alert wake consumer writes these rows.
"alert.wake_requested",
# KR-CC1-POLISH — auto-approve sweep for low-risk probe-fix-
# envelope proposals. Emitted by the post-cycle auto-approve
# sweep ONLY when:
# * The proposal's ``blast_radius_level == "low"`` (matches
# a known-narrow envelope action; see
# ``kora_cli/promote/probe_fix_envelopes/proposer.py``
# ``_KNOWN_LOW_RISK_PATTERNS``)
# * Operator opted in via
# ``KORA_PROMOTE_PROBE_FIX_AUTO_APPROVE_LOW_RISK=true``
# * The proposal has been pending ≥
# ``KORA_PROMOTE_PROBE_FIX_AUTO_APPROVE_WAIT_HOURS``
# (default 1h) — operator's window to manually reject
# Two-tier gating preserved: this seam means "the proposal is
# now in the envelope vocabulary"; actual fix-attempt execution
# STILL requires ``KORA_PROBE_AUTOFIX_<NAME>_ENABLED=true``.
# Payload mirrors ``promotion.probe_envelope_action_proposed``
# + adds ``auto_approve_wait_hours`` (the actual wait the
# sweep applied) + ``auto_approved_at`` (ISO ts) so operator
# triage can reconstruct the timeline.
"promotion.probe_envelope_action_auto_approved",
]

SourceName = Literal[
Expand Down
70 changes: 70 additions & 0 deletions kora_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10549,6 +10549,76 @@ def main():
)
fallback_parser.set_defaults(func=cmd_fallback)

# =========================================================================
# promote command — KR-CC1-POLISH (#198)
# =========================================================================
# Operator-facing surface for the 6 promotion loops (phrasebook /
# snapshot_expand / router_tuning / tool_trimming /
# probe_fix_envelopes / email_intent). Read-only subcommands
# (status / history / pending) + one ad-hoc trigger (run-once).
# Implementation lives in kora_cli/promote_cli.py so this main.py
# only carries argparse glue.
from kora_cli.promote_cli import LOOP_NAMES as _PROMOTE_LOOPS
from kora_cli.promote_cli import cmd_promote

promote_parser = subparsers.add_parser(
"promote",
help="Inspect + run-once Kora's promotion loops",
description=(
"Operator visibility into the 6 promotion loops: per-loop "
"pending/approved/rejected counts (status), full proposal "
"JSON (pending), recent audit-row history (history), and "
"ad-hoc cycle invocation (run-once). All subcommands "
"emit JSON to stdout — pipe through ``jq`` for queries."
),
)
promote_subparsers = promote_parser.add_subparsers(
dest="promote_command"
)

promote_subparsers.add_parser(
"status",
help="Per-loop counts + last activity timestamp",
)

promote_run_once = promote_subparsers.add_parser(
"run-once",
help="Invoke one cycle of a specific loop ad-hoc",
)
promote_run_once.add_argument(
"loop",
choices=list(_PROMOTE_LOOPS),
help="Loop name to invoke",
)

promote_history = promote_subparsers.add_parser(
"history",
help="Recent audit rows for a loop (default last 30 days)",
)
promote_history.add_argument(
"loop",
choices=list(_PROMOTE_LOOPS),
help="Loop name",
)
promote_history.add_argument(
"--days",
type=int,
default=30,
help="Lookback window in days (default 30)",
)

promote_pending = promote_subparsers.add_parser(
"pending",
help="JSON dump of currently-pending proposals for a loop",
)
promote_pending.add_argument(
"loop",
choices=list(_PROMOTE_LOOPS),
help="Loop name (snapshot_expand has no pending — use history)",
)

promote_parser.set_defaults(func=cmd_promote)

# =========================================================================
# gateway command
# =========================================================================
Expand Down
223 changes: 223 additions & 0 deletions kora_cli/promote/probe_fix_envelopes/auto_approve.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
"""Auto-approve sweep for low-risk envelope proposals — KR-CC1-POLISH.

Optional opt-in sweep that runs after the main probe-fix-envelope
cycle. Auto-approves proposals whose ``blast_radius_level == "low"``
after a configurable wait window (default 1h), giving the operator
a chance to manually reject before the auto-approve fires.

# Two-tier gating (CRITICAL)

This sweep auto-approves the *proposal* — i.e. the proposed
envelope action moves from ``pending/`` to ``approved/`` + an
audit row fires. It does NOT auto-EXECUTE the envelope action.
The envelope still requires the operator to flip the per-probe
enable env (``KORA_PROBE_AUTOFIX_<NAME>_ENABLED=true``, see
``kora_cli/probes/fix_envelopes.py``) before Kora's reasoning
loop will actually invoke the fix at runtime.

The two tiers are:
1. Auto-approve → "this is in our envelope vocabulary"
2. Per-probe ENABLED env → "Kora is permitted to invoke it"

# Env

* ``KORA_PROMOTE_PROBE_FIX_AUTO_APPROVE_LOW_RISK`` (default
``false``) — master opt-in. False = sweep is a no-op.
* ``KORA_PROMOTE_PROBE_FIX_AUTO_APPROVE_WAIT_HOURS`` (default
``1.0``) — minimum time a low-risk proposal must sit in
``pending/`` before auto-approval. Operator's review window.

# When to run

The sweep is invoked at the end of each probe-fix-envelope cycle
in ``plugin.py`` AFTER fresh proposals have been persisted. That
way a freshly-proposed low-risk proposal still spends its full
wait window in pending before the next sweep picks it up.
"""

from __future__ import annotations

import logging
import os
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import List

from kora_cli.promote._shared.proposal_store import (
list_by_status,
transition,
)

from .proposer import (
ProbeEnvelopeProposal,
proposal_from_dict,
proposal_to_dict,
)

logger = logging.getLogger(__name__)


LOOP_NAME = "probe_fix_envelopes"

AUTO_APPROVE_ENABLED_ENV = "KORA_PROMOTE_PROBE_FIX_AUTO_APPROVE_LOW_RISK"
AUTO_APPROVE_WAIT_HOURS_ENV = (
"KORA_PROMOTE_PROBE_FIX_AUTO_APPROVE_WAIT_HOURS"
)
DEFAULT_AUTO_APPROVE_WAIT_HOURS = 1.0


@dataclass(frozen=True, slots=True)
class AutoApproveSweepResult:
"""Per-sweep telemetry. Cycle aggregator stores ``approved_count``
in its summary so operator-grep can find sweep activity."""

candidates_considered: int # all low-risk pending
candidates_under_wait_window: int # low-risk but < wait_hours old
approved_count: int


def is_auto_approve_enabled() -> bool:
raw = os.environ.get(AUTO_APPROVE_ENABLED_ENV, "false").strip().lower()
return raw in {"true", "1", "yes", "on"}


def _read_wait_hours() -> float:
raw = os.environ.get(AUTO_APPROVE_WAIT_HOURS_ENV, "").strip()
if not raw:
return DEFAULT_AUTO_APPROVE_WAIT_HOURS
try:
value = float(raw)
except ValueError:
logger.warning(
"[kora.promote.probe_fix_envelopes.auto_approve] %s=%r not "
"numeric; using default %sh",
AUTO_APPROVE_WAIT_HOURS_ENV,
raw,
DEFAULT_AUTO_APPROVE_WAIT_HOURS,
)
return DEFAULT_AUTO_APPROVE_WAIT_HOURS
if value < 0:
return DEFAULT_AUTO_APPROVE_WAIT_HOURS
return value


def _emit_auto_approved_audit(
proposal: ProbeEnvelopeProposal, *, wait_hours: float, approved_at: datetime
) -> None:
"""Emit ``promotion.probe_envelope_action_auto_approved`` per
auto-approved proposal. Best-effort: any audit-write failure
logs + is swallowed (the transition already succeeded)."""
try:
from kora_cli.audit.jsonl_sink import emit_audit
except Exception as exc:
logger.warning(
"[kora.promote.probe_fix_envelopes.auto_approve] audit "
"import failed: %r — auto_approved row skipped",
exc,
)
return
payload = proposal_to_dict(proposal)
payload["status"] = "approved"
payload["auto_approve_wait_hours"] = round(wait_hours, 4)
payload["auto_approved_at"] = approved_at.strftime(
"%Y-%m-%dT%H:%M:%SZ"
)
try:
emit_audit(
"promotion.probe_envelope_action_auto_approved",
payload,
caller_session_id=(
f"promotion:probe_fix_envelopes:{proposal.proposal_id}"
),
source="reasoning",
)
except Exception as exc:
logger.warning(
"[kora.promote.probe_fix_envelopes.auto_approve] emit_audit "
"raised %r — proposal already transitioned",
exc,
)


def run_auto_approve_sweep(
*, now: datetime | None = None
) -> AutoApproveSweepResult:
"""Walk pending proposals + auto-approve low-risk ones whose age
crossed the wait window.

No-op when ``AUTO_APPROVE_ENABLED_ENV`` is falsy. Returns a
structured result so the caller (cycle aggregator) can surface
sweep activity in its summary log.
"""
if not is_auto_approve_enabled():
return AutoApproveSweepResult(
candidates_considered=0,
candidates_under_wait_window=0,
approved_count=0,
)

wait_hours = _read_wait_hours()
wait_seconds = wait_hours * 3600.0
now_dt = now or datetime.now(timezone.utc)

pending_payloads = list_by_status(
loop_name=LOOP_NAME, status="pending"
)
candidates: List[ProbeEnvelopeProposal] = []
for payload in pending_payloads:
try:
proposal = proposal_from_dict(payload)
except Exception as exc:
logger.warning(
"[kora.promote.probe_fix_envelopes.auto_approve] "
"proposal_from_dict raised %r — skipping",
exc,
)
continue
if proposal.blast_radius_level != "low":
continue
candidates.append(proposal)

eligible: List[ProbeEnvelopeProposal] = []
under_window = 0
for proposal in candidates:
age_seconds = (now_dt - proposal.created_at).total_seconds()
if age_seconds < wait_seconds:
under_window += 1
continue
eligible.append(proposal)

approved = 0
for proposal in eligible:
try:
transition(
loop_name=LOOP_NAME,
proposal_id=proposal.proposal_id,
new_status="approved",
payload_mutator=lambda p: p.update(
{
"status": "approved",
"review_notes": (
f"auto-approved (low-risk; "
f"{wait_hours:.2f}h wait window)"
),
}
),
)
except Exception as exc:
logger.warning(
"[kora.promote.probe_fix_envelopes.auto_approve] "
"transition raised %r for %s — skipping",
exc,
proposal.proposal_id,
)
continue
_emit_auto_approved_audit(
proposal, wait_hours=wait_hours, approved_at=now_dt
)
approved += 1
return AutoApproveSweepResult(
candidates_considered=len(candidates),
candidates_under_wait_window=under_window,
approved_count=approved,
)
24 changes: 24 additions & 0 deletions kora_cli/promote/probe_fix_envelopes/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,30 @@ async def run_probe_fix_envelopes_cycle(
exc,
)

# KR-CC1-POLISH — auto-approve sweep AFTER fresh proposals
# land. Order matters: a freshly-proposed low-risk proposal
# spends its full wait window in pending before the NEXT
# cycle's sweep picks it up. Sweep is a no-op when the
# operator env opt-in is falsy (default).
try:
from .auto_approve import run_auto_approve_sweep

sweep = run_auto_approve_sweep(now=started_dt)
summary["auto_approved_low_risk_count"] = sweep.approved_count
summary["auto_approve_candidates_considered"] = (
sweep.candidates_considered
)
summary["auto_approve_under_wait_window"] = (
sweep.candidates_under_wait_window
)
except Exception as exc:
logger.warning(
"[kora.promote.probe_fix_envelopes] auto_approve sweep "
"raised %r — cycle continues",
exc,
)
summary["auto_approved_low_risk_count"] = 0

summary["duration_ms"] = int(
(time.monotonic() - started_monotonic) * 1000
)
Expand Down
Loading
Loading