Skip to content

Finding lifecycle

The internal/findinglifecycle subsystem ingests durable findings, enforces a closed LifecycleState transition graph, and emits paired finding_lifecycle.action slog records via a bounded LifecycleAction enum (internal/findinglifecycle/lifecycle_emit.go). Records are written to slog and a process-scoped ring buffer (capacity 16) so operators and agents can inspect recent activity without scraping logs.

The full enum lives in internal/findinglifecycle/lifecycle_emit.go. Adding an action requires updating severityForLifecycleAction, the exhaustive enum test, and the ### Finding lifecycle recipe block in docs/topics/agent/tracing.md.

ActionSeverityMeaning
ingest_skippedinfoDiagnostic batch was non-authoritative; no rows ingested.
ingest_failedwarnIngest pipeline failed (validation or store).
ingest_scope_failedwarnWorkspace scope resolution failed.
ingest_pool_failedwarnWorker-pool start failed; ingest disabled.
ingest_worker_submit_failedwarnWorker-pool submit rejected the ingest job.
link_persist_failedwarnCross-finding link batch failed to persist.
validation_findings_ingestedinfoVerify pass ingested operation findings.
validation_findings_resolvedinfoVerify pass resolved one or more findings.

finding_lifecycle.action records carry feature=finding_lifecycle, the bounded action enum, and metadata-only attributes (session_id, finding_count, lifecycle_status, reason, error, debounced, count). Bodies and diagnostic source ranges never enter the ring.

The closed state machine lives in internal/findinglifecycle/types.go:

StateMeaning
openDefault on first ingest; visible to read paths.
resolvedClosed verdict; remains visible.
suppressedHidden from the default read path.
accepted_riskVisible with an explicit risk badge.

Illegal transitions are rejected at ApplyTransition; the graph is exhaustively property-tested (internal/findinglifecycle/transition_graph_test.go).

Aliases: /finding-lifecycle, /finding_lifecycle. Read-only. Renders ingest counters (InFlight, FailureCount, last success / failure timestamps), the most-recent ring entries, and a one-line summary the sidebar reuses.

Same snapshot as /findings for the current process. CLI and TUI share the ring only when run in the same process (the ring is process-global, not persisted). A regression test enforces byte-equal JSON parity between the CLI and the App-level snapshot (internal/cmd/config_cmd_show_finding_lifecycle_test.go).

The TUI sidebar shows Findings: with one of:

  • empty — no in-flight ingest and no recent warn/error events.
  • ingest x<n><n> ingest passes are currently in flight.
  • warn: <action> — the most recent ring entry was warn or error severity.

Permissive session filter: events without a session id are included alongside the current session, so cross-session worker failures still surface.

list_recent_finding_lifecycle_events returns JSON { "ring_capacity", "returned", "events" } with snake_case event fields. Optional session_id filter and limit (1..16, default ring capacity). The same data drives /findings and bmo config show-finding-lifecycle.

list_findings (read-only, ExposeFirst) lists the per-session finding rows the read path exposes after the trust matrix filter.

bmo_list_findings and bmo_list_recent_finding_lifecycle_events expose the same JSON contracts to MCP-host agents (Cursor, Claude Desktop, etc.). Parity with the in-process agent tools is enforced by internal/mcp/tools/findings_test.go.

  • Pool start failure on app boot. ingest_pool_failed (Warn) at startup; subsequent ingest calls become no-ops. The sidebar carries the warn label until the next session start.
  • Cross-finding link persistence failure. link_persist_failed (Warn) when a batch flush hits the store; ingest continues for new findings.
  • Diagnostic ingest failed mid-flight. ingest_failed (Warn) with session_id, finding_count, debounced, error. Pair the preceding ingest_skipped (Info) records by session_id to see whether the same session was previously short-circuited by the trust gate.

Lifecycle records carry only metadata. Specifically out of scope:

  • Finding body text and operator notes
  • Diagnostic source-range contents
  • File paths beyond the workspace-scoped session id

The ring truncates error_summary and lifecycle_status to keep cardinality predictable. If a future arm needs a structured signal, add it as a new LifecycleAction enum constant — do not stuff data into free-text attributes.