missions: phase-completion summary card (Claude Opus 4.8 synthesized)
New phase_summarizer background worker fires on any mission_phase
transition to a terminal state (completed/failed). Aggregates every
topology_runs.checkpoint.outputs[] + mission_tasks + mission_artifacts
bound to that phase and asks Claude Opus 4.8 to produce a structured
JSON card:
{ narrative, metrics, sources, tooling, next_actions }
Rendered inline on the mission page under each completed phase via
new PhaseSummaryCard component. Metrics grid is kind-specific:
research surfaces insights/sources/int_cards/artifacts, coding
surfaces cards_picked_up/commits/tests/issues, benchmark surfaces
regressions/improvements, security surfaces findings-by-severity.
New table: mission_phase_summaries (migration 0060), unique per
phase_id — regenerates on retry.
New endpoint: GET /api/missions/{id}/phases/{phase_id}/summary.
Model overridable via CLAWMATES_SUMMARIZER_MODEL. Reuses the
ANTHROPIC_API_KEY prod already carries for mission_refiner.
This commit is contained in:
@@ -480,6 +480,51 @@ pub async fn retry_phase(
|
||||
Ok(Json(serde_json::json!({ "reset": true })))
|
||||
}
|
||||
|
||||
/// GET /api/missions/{id}/phases/{phase_id}/summary — the completion
|
||||
/// card produced by `phase_summarizer` for a terminal-state phase.
|
||||
/// Returns 404 while the phase is still running / hasn't been
|
||||
/// summarized yet.
|
||||
pub async fn get_phase_summary(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Path((id, phase_id)): Path<(Uuid, Uuid)>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
// Scope check.
|
||||
let _ = cm_db::repo::missions::get(&state.pool, id, user.workspace_id.as_uuid())
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
use sqlx::Row;
|
||||
let row = sqlx::query(
|
||||
"SELECT kind, model, narrative, metrics, sources, artifacts,
|
||||
tooling, next_actions, generated_at, error
|
||||
FROM mission_phase_summaries
|
||||
WHERE mission_id = $1 AND phase_id = $2",
|
||||
)
|
||||
.bind(id)
|
||||
.bind(phase_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?;
|
||||
let Some(r) = row else {
|
||||
return Err(ApiError::NotFound);
|
||||
};
|
||||
let generated_at: time::OffsetDateTime = r.get("generated_at");
|
||||
let payload = serde_json::json!({
|
||||
"kind": r.get::<String, _>("kind"),
|
||||
"model": r.get::<String, _>("model"),
|
||||
"narrative": r.get::<String, _>("narrative"),
|
||||
"metrics": r.get::<Value, _>("metrics"),
|
||||
"sources": r.get::<Value, _>("sources"),
|
||||
"artifacts": r.get::<Value, _>("artifacts"),
|
||||
"tooling": r.get::<Value, _>("tooling"),
|
||||
"next_actions": r.get::<Value, _>("next_actions"),
|
||||
"generated_at": generated_at
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.unwrap_or_default(),
|
||||
"error": r.get::<Option<String>, _>("error"),
|
||||
});
|
||||
Ok(Json(payload))
|
||||
}
|
||||
|
||||
/// GET /api/missions/{id}/runs — topology_runs bound to this mission,
|
||||
/// newest first. Used by the Live tab to subscribe to per-run SSE.
|
||||
pub async fn list_runs(
|
||||
|
||||
Reference in New Issue
Block a user