diff --git a/README.md b/README.md index 7786359..00ff1aa 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # tutti -**Multi-agent orchestration for AI coding tools. Coordinate Claude Code, Codex, and Aider agents as a team — with a real-time web dashboard, automated SDLC workflows, and per-agent git worktree isolation.** +**Tutti is the operator layer for multi-agent coding workflows.** It runs Claude Code, Codex, and Aider as a coordinated team, gives each agent its own git worktree, and shows you what’s happening in real time. ```bash cargo install tutti ``` -Tutti spawns multiple AI coding agents in tmux sessions, gives each one its own git worktree, and orchestrates them through configurable workflows — plan, implement, test, review, ship. A web dashboard shows every agent's status in real time. Click any agent to see its live terminal output, token usage, and code changes. +Use Tutti when one agent is no longer the bottleneck, but unmanaged parallelism is. It handles session orchestration, workflow dispatch, and operator visibility so you stop doing the routing, tracking, and merge management by hand. ![Factory floor — 5 agents working through an SDLC pipeline](docs/images/factory-floor.png) @@ -29,6 +29,19 @@ tt run sdlc-auto # run a full plan→implement→test→review→ship **Prerequisites:** Rust toolchain, tmux, and at least one AI coding CLI installed (Claude Code, Codex, or Aider). +## Why Tutti exists + +If you already use coding agents, the pain is rarely “the model is bad.” +It’s usually one of these: + +- you are manually routing work between multiple agent sessions +- you lose track of which agent is doing what +- agents step on each other’s changes +- review, test, and handoff loops are inconsistent +- parallel work creates more supervision overhead than it saves + +Tutti fixes that layer. + ## What Tutti Does - **Spawns and manages** multiple AI coding agent sessions (Claude Code, Codex, Aider) in tmux @@ -85,7 +98,7 @@ wait_for_idle = true Version it. Share it. Fork someone else's. -## Quick Start +## Install from source ```bash # Install from crates.io @@ -307,6 +320,7 @@ Reusable prompt components and skills are **phrases**. A phrase might be a CLAUD ### Automation (Built) - `tt run` / `tt verify` reusable workflow execution with persisted run records +- `tt runs activation` reports whether a workspace has reached its first successful workflow run - Run checkpoints persisted at `.tutti/state/workflow-checkpoints/.json` + `tt run --resume ` - Workflow step types: `prompt`, `command`, `ensure_running`, `workflow` (nested), `review`, `land` - Workflow `review`/`land` steps auto-start required sessions when they are not already running diff --git a/docs/tutti-activation-telemetry-plan-2026-05-05.md b/docs/tutti-activation-telemetry-plan-2026-05-05.md new file mode 100644 index 0000000..3315525 --- /dev/null +++ b/docs/tutti-activation-telemetry-plan-2026-05-05.md @@ -0,0 +1,62 @@ +# Tutti Activation Telemetry Plan — 2026-05-05 + +## Goal +Define the minimum activation signal for Tutti without adding analytics bloat. + +## Recommendation +Use the existing control-plane event stream and run ledger. + +Do not build a separate telemetry system for this sprint. + +## Activation definition +A workspace is **activated** when it reaches its **first successful orchestrated workflow run**. + +Recommended exact signal: +- first `workflow.completed` event where: + - the run belongs to a real workspace + - the workflow is user-triggered or first-run guided + - the run result is successful + +## Activation funnel for this sprint +Track only these milestones: +1. `tt init` completed +2. `tt up` succeeded +3. first workflow started (`workflow.started`) +4. first workflow completed successfully (`workflow.completed`) + +That is enough to answer: +- are installs reaching first value? +- where are they dropping? +- is onboarding improving activation? + +## What already exists +Current Tutti already has: +- persisted control events in `.tutti/state/events.jsonl` +- workflow lifecycle events including `workflow.started` and `workflow.completed` +- run records and local API endpoints for runs/events +- SSE/event stream exposure via `/v1/events` and `/v1/events/stream` + +## Sprint scope for #25 +For this sprint, keep it narrow: +- document the activation definition +- ensure the docs point to the existing event sources +- if needed later, add one lightweight helper command or doc example for querying first successful run + +## Explicit non-goals +Not this sprint: +- third-party product analytics +- cohort dashboards +- hosted telemetry backend +- deep retention instrumentation +- broad event schema redesign + +## Suggested next implementation slice +If we want one concrete engineering follow-up after this doc: +- add a small operator-facing command or example script that answers: + - "Has this workspace activated yet?" + - "When was the first successful workflow run?" + +## Why this is the right cut +Tutti already emits the important events. +The problem is not missing telemetry plumbing. +The problem is missing activation definition and operator-facing usage of the data already there. diff --git a/docs/tutti-gstack-review-2026-05-05.md b/docs/tutti-gstack-review-2026-05-05.md new file mode 100644 index 0000000..55bef89 --- /dev/null +++ b/docs/tutti-gstack-review-2026-05-05.md @@ -0,0 +1,232 @@ +# Tutti — GStack-style Product Review + +Date: 2026-05-05 +Reviewer: Wren +Method: local OpenClaw GStack scaffold (`office-hours` + `ceo-review` style synthesis) + +## Bottom line + +**Recommendation: Accelerate, but narrow.** + +Tutti has crossed the line from "clever internal tool" to **real emerging product signal**. The stars, forks, merged PR cadence, green Actions, and issue depth mean this is no longer just a private toy. But the risk now is classic founder overreach: building the whole future of agent operations before nailing the one thing that makes people trust it. + +The right move is **not** to broaden the roadmap. The right move is to make Tutti feel undeniably reliable for the operator managing multi-agent coding work. + +--- + +## 1) User problem + +Tutti solves a very real and increasingly painful problem: + +> Once you have more than one coding agent, coordination becomes the bottleneck. + +The pain is not "I need a smarter model." +It is: +- routing work across agents +- keeping worktrees isolated +- seeing what each agent is doing +- recovering from stalls, auth failures, and provider weirdness +- turning ad hoc multi-agent chaos into repeatable workflows + +This is a sharp problem. It exists today. And it gets worse as more people try to orchestrate Claude Code, Codex, Aider, and OpenClaw together. + +--- + +## 2) Target user + +Primary user: +- technical power user +- founder/operator/dev who already lives in terminals and repos +- already using multiple agents or clearly about to +- feels the pain of manual coordination, not just model quality + +Not the primary user right now: +- average solo dev with one agent +- enterprise buyer wanting polished compliance theater first +- anyone who still needs to be convinced that agent-based coding is worth doing + +This is a product for **people already over the threshold**. + +--- + +## 3) Why now + +Because multi-agent coding just became legible enough to hurt. + +A year ago, this would have been too early and too weird. +Now the ingredients exist: +- agent CLIs are real +- people are running multiple sessions +- providers fail in annoying, visible ways +- orchestration and observability are the missing layer + +The repo traction matters because it suggests this pain is not private. **32 stars and 2 forks for a niche agent-ops tool is non-trivial**, especially with recent PR flow and active issue evolution. + +This is still early, but the timing is good. + +--- + +## 4) Differentiation + +Tutti's differentiation is strongest when positioned as: + +> **The operator layer for multi-agent coding workflows** + +Not another agent. +Not a general AI platform. +Not an LLM wrapper. + +The strongest differentiators visible right now are: +- orchestration across existing agent CLIs +- per-agent worktree isolation +- workflow automation for SDLC paths +- real-time operator visibility / dashboard +- resilience and recovery posture +- issue-claim and multi-step automation framing + +That is a coherent wedge. + +Where differentiation gets blurry: +- if the message becomes "everything for all agent workflows" +- if roadmap breadth outruns the operator-core product truth +- if it starts sounding like generic autonomy theater + +--- + +## 5) Smallest magical product + +The smallest magical version of Tutti is **not** the full roadmap. +It is this: + +> A developer launches multiple agents, sees them clearly, routes work confidently, and can trust the system not to silently stall or lose the thread. + +Concretely, the magical core is: +- reliable session orchestration +- operator console / dashboard visibility +- deterministic workflow execution +- strong stall/recovery detection +- confidence that a run either progresses or fails clearly + +This is why issue **#122** matters so much. A spinner-detection miss sounds small, but it attacks the product's deepest promise: **can I trust the orchestration layer?** + +--- + +## 6) Traction interpretation + +Current signals checked today: +- 32 stars +- 2 forks +- recent merged PR activity +- green Actions +- no open PR backlog +- live issue queue with substantive product/system discussions + +Interpretation: +- This is **real early traction**, not breakout traction. +- The most important part is not the absolute number. It is the combination of: + - external attention + - continued maintenance velocity + - evidence of product thinking in issues + - visible movement in releases and workflow polish + +The forks matter because they imply at least some people want to do more than spectate. +That’s a stronger signal than passive stars. + +But don’t overread it. This is a **promising wedge**, not proof of durable adoption yet. + +--- + +## 7) Biggest failure modes + +### A. Reliability gap kills trust +If orchestration says "idle" while the agent is actually working, the whole product feels flaky. +This is existentially bad for an operator tool. + +### B. Roadmap sprawl +Mailbox, uploads, remote access, approvals, cost guardrails, release finalizers, RBAC, deployment story, trust pack — all sensible individually, but together they can blur the near-term product spine. + +### C. Positioning drift +If Tutti is described too broadly, people will not know whether it is for: +- agent ops +- remote coding +- enterprise control +- workflow automation +- dashboarding +- autonomy infrastructure + +The repo already hints at the right answer: **agent operations layer**. Stay there. + +### D. Premature enterprise surface area +Identity/RBAC/compliance/deployment stories matter later, but they are not the reason current users care. + +### E. Coordination tax outweighs delight +If the setup, workflow config, or mental overhead is too high, only the already-convinced will persist. + +--- + +## 8) What to exclude for now + +Deliberately de-prioritize, unless pulled by direct user demand: +- full enterprise trust/compliance packaging +- broad RBAC/org model work +- expansive platform messaging +- overbuilt remote/media bridge before core reliability is sharp +- autonomy flourish that is impressive but not trusted + +This does **not** mean these are bad ideas. +It means they are not the best immediate use of scarce founder attention. + +--- + +## 9) Recommended immediate focus + +### Keep / kill / narrow / accelerate +- **Keep:** operator console, orchestration reliability, workflow repeatability +- **Kill:** any urge to market this as a broad universal AI operations platform right now +- **Narrow:** roadmap emphasis to trust + operator clarity + first-run proof of value +- **Accelerate:** the fixes and UX improvements that make real dogfooding feel solid + +### Highest-leverage issue sequence +1. **#122** — spinner/activity detection reliability +2. **#24** — positioning and onboarding narrative +3. **#25** — telemetry and activation instrumentation +4. **#56** — agent mailbox for coordination +5. **#83** — context bridge / uploads to remote agents + +Why this order: +- #122 protects trust +- #24 improves comprehension +- #25 makes learning measurable +- #56 deepens the core product +- #83 expands surface area after the core feels real + +--- + +## 10) Recommended next sprint + +If I were running Tutti for the next sprint, I would make it: + +### Sprint theme +**"Make the operator trust it."** + +### Sprint goals +- fix workflow/idle-detection reliability papercuts +- tighten the operator story in README/onboarding +- instrument the first-success path so traction becomes legible + +### Success criteria +- a dogfood run no longer false-fails on visible agent activity +- a new technically literate user can explain Tutti in one sentence after the README +- you can measure whether installs reach first successful workflow run + +--- + +## Final opinionated take + +Tutti is interesting because it is **practical**, not because it is futuristic. +That’s the asset. + +Do not drown it in platform ambition. +Make it the most trustworthy way to run a small team of coding agents. +If you nail that, the rest of the roadmap gets easier and the traction compounds. +If you don’t, the roadmap becomes expensive decoration. diff --git a/docs/tutti-issue-triage-2026-05-05.md b/docs/tutti-issue-triage-2026-05-05.md new file mode 100644 index 0000000..674f883 --- /dev/null +++ b/docs/tutti-issue-triage-2026-05-05.md @@ -0,0 +1,69 @@ +# Tutti Issue Triage — 2026-05-05 + +## Summary +Tutti has crossed the line from interesting internal tool to emerging real product. The issue queue should now optimize for **trust, activation, and core orchestration value** — in that order. + +## Now + +### 1. #122 — wait_for_prompt_activity spinner detection bug +**Why now:** This is the highest-leverage issue because it damages operator trust in the orchestration layer itself. If Tutti falsely thinks agents are idle or failed while they are actively thinking, the whole product feels flaky. + +**Impact:** Reliability, confidence, dogfooding quality, outside adoption. + +### 2. #24 — Positioning and onboarding narrative +**Why now:** Traction is starting to appear. People need to understand what Tutti is for, when to use it, and why it beats ad hoc multi-agent chaos. + +**Impact:** Activation, category clarity, forks/stars conversion. + +### 3. #25 — Telemetry and activation instrumentation +**Why now:** Traction without instrumentation is vibes. This issue turns outside interest and internal dogfooding into measurable insight. + +**Impact:** Learning speed, activation tuning, prioritization quality. + +## Next + +### 4. #56 — agent mailbox for inter-agent coordination +**Why next:** This is a true core product primitive. It deepens the orchestration moat, but it should land after trust and activation basics are stronger. + +**Impact:** Product depth, reduced human relay burden, stronger coordination story. + +### 5. #83 — context bridge / file-image upload to remote agents +**Why next:** High user value, especially for remote-first/VPS workflows. But it is less important than operator trust and clear proof of value. + +**Impact:** Remote workflow usability, differentiated value. + +### 6. #36 — `tt run --auto-allow` bootstrap mode +**Why next:** Useful for smoother autonomous workflow adoption, but only once core operator trust is stronger. + +### 7. #27 — post-merge release finalizer +**Why next:** Good automation hygiene, but not the primary adoption bottleneck. + +## Later +- #20 — cost escalation and autonomy guardrails +- #18 — approval gates and guardrail workflows +- #17 — unattended runtime recovery and provider auto-switch +- #23 — trust and compliance pack +- #21 — deployment and operations story +- #15 — identity, auth, and RBAC foundations + +These matter, but they are later-stage multipliers. They should not outrank immediate trust, onboarding, and activation. + +## Top 5 ranked issues +1. **#122** — reliability/trust bug in prompt activity detection +2. **#24** — onboarding and positioning narrative +3. **#25** — telemetry and activation instrumentation +4. **#56** — agent mailbox +5. **#83** — context bridge uploads + +## Missing issue that should exist +A focused issue for **"first 10 minutes activation proof"** should probably exist if it does not already. Positioning (#24) is about narrative, but Tutti also needs an explicit product issue aimed at making the first-run experience undeniably useful. + +Suggested issue: +- Improve time-to-value in first session +- Make one workflow/demo path obviously magical +- Reduce setup friction and ambiguity +- Define the exact signal: first successful orchestrated workflow within 10–15 minutes + +## Recommendation +If only one engineering issue gets worked next, it should be **#122**. +If one product issue gets worked in parallel, it should be **#24**. diff --git a/docs/tutti-local-audit-2026-05-05.md b/docs/tutti-local-audit-2026-05-05.md new file mode 100644 index 0000000..7b29b8d --- /dev/null +++ b/docs/tutti-local-audit-2026-05-05.md @@ -0,0 +1,77 @@ +# Tutti Local Repo Audit — 2026-05-05 + +## Executive summary +Tutti looks like a real, fast-moving product repo rather than a half-formed experiment. The combination of healthy README positioning, recent merged PR velocity, green Actions, and a clear architecture/story suggests genuine momentum. + +The biggest risk is not technical collapse. It is **scope sprawl outrunning operator trust and activation clarity**. + +## Current repo signals +- Repo: `nutthouse/tutti` +- Public repo +- Default branch: `main` +- GitHub signals checked separately: 32 stars, 2 forks, recent merged PR activity, green Actions, no open PR backlog +- Local checkout branch: `main` tracking `origin/main` +- Two local untracked empty files exist: `...` and `merged` + - Both appear benign junk residue, not structural risk + +## Architecture shape +Based on the README and manifest, Tutti is a Rust CLI centered around: +- orchestration core +- runtime adapters for coding agents +- terminal/tmux session layer +- observation layer +- optional dashboard / local control API + +This is a credible shape for the product category. It matches the stated wedge: orchestrate existing AI coding agents instead of becoming one. + +## Product maturity signals +Strong signals: +- multi-runtime support is already framed clearly +- web dashboard and operator-console concepts are tangible +- automated SDLC language is concrete, not hand-wavy +- recent PR stream suggests active iteration, not stagnation +- issue taxonomy is coherent enough to read like a real product roadmap + +Less mature / risk signals: +- versioning/docs may lag reality in places (`Cargo.toml` shows `0.6.0` while README references later project status/history) +- roadmap breadth is large enough to dilute focus +- several later-stage platform concerns are open before the trust/activation loop feels fully locked + +## Verification notes +I was able to inspect: +- `README.md` +- `Cargo.toml` +- recent merged PRs +- open issues +- repo state / branch state + +A fuller `cargo test` pass was not completed here because approval-gated shell execution interrupted the deeper audit. So this is a grounded inspection report, not a build-verification report. + +## Technical risks +1. **Operator trust bugs** + - #122 is the best example. Reliability papercuts undermine the product more than missing advanced features. + +2. **Mismatch between story and first-run experience** + - The README promise is ambitious. If first-run value is weaker than the pitch, traction leaks. + +3. **Scope spread across too many frontier features** + - Remote uploads, mailbox, approvals, recovery, RBAC, deployment, trust pack: all sensible, but too many “important” fronts at once. + +4. **Potential docs/version drift** + - Signals of fast movement are good, but product status and package version should stay crisp to preserve trust. + +## Best next sprint from an engineering perspective +### Theme: make the operator trust Tutti +Recommended sprint contents: +- Fix **#122** spinner/activity detection bug +- Tighten one adjacent reliability edge if found during implementation +- Make sure the visible operator story matches current reality +- Avoid starting a deep new systems feature in the same sprint + +## What to avoid this week +- starting mailbox + uploads + guardrails all at once +- deep enterprise work (RBAC, compliance pack) before activation is sharper +- adding product surface area faster than reliability + +## Recommendation +Tutti is healthy enough to treat seriously. The next sprint should **narrow hard around reliability + activation clarity**, not chase breadth. diff --git a/src/automation/mod.rs b/src/automation/mod.rs index cdb38ea..a1aa2bb 100644 --- a/src/automation/mod.rs +++ b/src/automation/mod.rs @@ -6,7 +6,7 @@ use crate::error::{Result, TuttiError}; use crate::health; use crate::health::WaitFailureReason; use crate::permissions::evaluate_command_policy; -use crate::runtime::{self, AgentStatus}; +use crate::runtime::{self, AgentStatus, RuntimeAdapter}; use crate::session::TmuxSession; use crate::state::{ AutomationRunRecord, ControlEvent, VerifyLastSummary, WorkflowStepIntentRecord, @@ -2568,12 +2568,15 @@ fn wait_for_prompt_activity_or_output( let pane = TmuxSession::capture_pane(session_name, PROMPT_CAPTURE_LINES)?; let pane_hash = hash_output(&pane); - let consumed = baseline_pane_hash.is_some_and(|baseline| pane_hash != baseline) - && snippet.as_ref().is_none_or(|needle| !pane.contains(needle)); + let consumed = pane_indicates_prompt_activity( + adapter.as_deref(), + &pane, + pane_hash, + baseline_pane_hash, + snippet.as_deref(), + ); - if let Some(adapter) = &adapter - && matches!(adapter.detect_status(&pane), AgentStatus::Working) - { + if pane_indicates_runtime_working(adapter.as_deref(), &pane) { return Ok(true); } @@ -2624,6 +2627,22 @@ fn hash_output(output: &str) -> u64 { hasher.finish() } +fn pane_indicates_runtime_working(adapter: Option<&dyn RuntimeAdapter>, pane: &str) -> bool { + adapter.is_some_and(|adapter| matches!(adapter.detect_status(pane), AgentStatus::Working)) +} + +fn pane_indicates_prompt_activity( + adapter: Option<&dyn RuntimeAdapter>, + pane: &str, + pane_hash: u64, + baseline_pane_hash: Option, + snippet: Option<&str>, +) -> bool { + let consumed = baseline_pane_hash.is_some_and(|baseline| pane_hash != baseline) + && snippet.is_none_or(|needle| !pane.contains(needle)); + consumed || pane_indicates_runtime_working(adapter, pane) +} + fn prompt_step_has_branch_progress(project_root: &Path, agent: &str) -> Result { let branch_file = project_root .join(".tutti") @@ -5576,4 +5595,62 @@ mod tests { assert!(matches!(resolved.steps[3], ResolvedStep::Land { .. })); let _ = std::fs::remove_dir_all(&dir); } + + #[test] + fn pane_activity_detects_claude_working_status_with_prompt_bar_present() { + let adapter = runtime::get_adapter("claude-code", None).unwrap(); + let pane = r#" +⏺ Searching for 1 pattern… (ctrl+o to expand) + ⎿ "budget: None," + +✳ Scurrying… (4m 5s · ↑ 783 tokens) + ⎿ Tip: Use /btw to ask a quick side question without interrupting Claude's current work + +──────────────────────────────────────────────────────────────────────────────── +❯ +──────────────────────────────────────────────────────────────────────────────── + ⏵⏵ don't ask on (shift+tab to cycle) · esc to interrupt +"#; + + let baseline = hash_output("before prompt submit"); + let pane_hash = hash_output(pane); + + assert!(pane_indicates_runtime_working(Some(adapter.as_ref()), pane)); + assert!(pane_indicates_prompt_activity( + Some(adapter.as_ref()), + pane, + pane_hash, + Some(baseline), + Some("Implement the fix") + )); + } + + #[test] + fn pane_activity_detects_consumed_prompt_without_runtime_signal() { + let pane = "Done writing files\n"; + let baseline = hash_output("previous pane with prompt still visible"); + let pane_hash = hash_output(pane); + + assert!(pane_indicates_prompt_activity( + None, + pane, + pane_hash, + Some(baseline), + Some("Implement the fix") + )); + } + + #[test] + fn pane_activity_rejects_unchanged_prompt_without_working_signal() { + let pane = "Implement the fix\n"; + let pane_hash = hash_output(pane); + + assert!(!pane_indicates_prompt_activity( + None, + pane, + pane_hash, + Some(pane_hash), + Some("Implement the fix") + )); + } } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index ae8ce7e..3d33736 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -401,6 +401,8 @@ pub enum WorkspacesSubcommand { pub enum RunsSubcommand { /// List all tracked SDLC runs List, + /// Show whether this workspace has reached first successful workflow activation + Activation, /// Show detailed view of a single run Show { /// Run ID to inspect diff --git a/src/cli/runs.rs b/src/cli/runs.rs index e85c4eb..30dbcc1 100644 --- a/src/cli/runs.rs +++ b/src/cli/runs.rs @@ -1,6 +1,17 @@ use crate::error::{Result, TuttiError}; -use crate::state::{load_active_runs, load_run_steps, load_sdlc_run_ledger}; +use crate::state::{ControlEvent, load_active_runs, load_control_events, load_run_steps, load_sdlc_run_ledger}; +use chrono::{DateTime, Utc}; use comfy_table::{Table, presets::UTF8_BORDERS_ONLY}; +use serde::Serialize; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +struct ActivationSummary { + activated: bool, + first_started_at: Option>, + first_completed_at: Option>, + workflow_name: Option, + run_id: Option, +} pub fn list() -> Result<()> { let cwd = std::env::current_dir()?; @@ -36,6 +47,43 @@ pub fn list() -> Result<()> { Ok(()) } +pub fn activation() -> Result<()> { + let cwd = std::env::current_dir()?; + let (_config, config_path) = crate::config::TuttiConfig::load(&cwd)?; + let project_root = config_path.parent().ok_or_else(|| { + TuttiError::ConfigValidation("could not determine workspace root".to_string()) + })?; + + let events = load_control_events(project_root)?; + let summary = summarize_activation(&events); + + println!( + "Activated: {}", + if summary.activated { "yes" } else { "no" } + ); + println!( + "First workflow started: {}", + summary + .first_started_at + .map(|ts| ts.to_rfc3339()) + .unwrap_or_else(|| "--".to_string()) + ); + println!( + "First successful workflow: {}", + summary + .first_completed_at + .map(|ts| ts.to_rfc3339()) + .unwrap_or_else(|| "--".to_string()) + ); + println!( + "Workflow: {}", + summary.workflow_name.unwrap_or_else(|| "--".to_string()) + ); + println!("Run: {}", summary.run_id.unwrap_or_else(|| "--".to_string())); + + Ok(()) +} + pub fn show(run_id: &str) -> Result<()> { let cwd = std::env::current_dir()?; let (_config, config_path) = crate::config::TuttiConfig::load(&cwd)?; @@ -225,11 +273,41 @@ fn format_issue(run: &crate::state::SdlcRunLedgerRecord) -> String { } } +fn summarize_activation(events: &[ControlEvent]) -> ActivationSummary { + let first_started = events + .iter() + .filter(|event| event.event == "workflow.started") + .min_by_key(|event| event.timestamp); + + let first_completed = events + .iter() + .filter(|event| event.event == "workflow.completed") + .min_by_key(|event| event.timestamp); + + ActivationSummary { + activated: first_completed.is_some(), + first_started_at: first_started.map(|event| event.timestamp), + first_completed_at: first_completed.map(|event| event.timestamp), + workflow_name: first_completed.and_then(workflow_name_from_event), + run_id: first_completed.map(|event| event.correlation_id.clone()), + } +} + +fn workflow_name_from_event(event: &ControlEvent) -> Option { + event + .data + .as_ref() + .and_then(|data| data.get("workflow_name")) + .and_then(|value| value.as_str()) + .map(|value| value.to_string()) +} + #[cfg(test)] mod tests { use super::*; use crate::state::{SdlcRunLedgerRecord, SdlcRunState}; - use chrono::Utc; + use chrono::{TimeZone, Utc}; + use serde_json::json; fn stub_record(issue_title: Option<&str>) -> SdlcRunLedgerRecord { SdlcRunLedgerRecord { @@ -304,4 +382,60 @@ mod tests { ledger.resume_eligible = true; assert!(derive_next_action(&ledger, &[]).contains("tt run --resume")); } + + #[test] + fn summarize_activation_reports_first_successful_workflow() { + let events = vec![ + ControlEvent { + event: "workflow.started".to_string(), + workspace: "demo".to_string(), + agent: None, + timestamp: Utc.with_ymd_and_hms(2026, 5, 5, 5, 0, 0).unwrap(), + correlation_id: "run-1".to_string(), + data: Some(json!({"workflow_name": "verify"})), + }, + ControlEvent { + event: "workflow.completed".to_string(), + workspace: "demo".to_string(), + agent: None, + timestamp: Utc.with_ymd_and_hms(2026, 5, 5, 5, 2, 0).unwrap(), + correlation_id: "run-1".to_string(), + data: Some(json!({"workflow_name": "verify", "success": true})), + }, + ControlEvent { + event: "workflow.completed".to_string(), + workspace: "demo".to_string(), + agent: None, + timestamp: Utc.with_ymd_and_hms(2026, 5, 5, 5, 5, 0).unwrap(), + correlation_id: "run-2".to_string(), + data: Some(json!({"workflow_name": "sdlc-auto", "success": true})), + }, + ]; + + let summary = summarize_activation(&events); + assert!(summary.activated); + assert_eq!(summary.run_id.as_deref(), Some("run-1")); + assert_eq!(summary.workflow_name.as_deref(), Some("verify")); + assert_eq!( + summary.first_completed_at, + Some(Utc.with_ymd_and_hms(2026, 5, 5, 5, 2, 0).unwrap()) + ); + } + + #[test] + fn summarize_activation_reports_not_activated_without_success() { + let events = vec![ControlEvent { + event: "workflow.started".to_string(), + workspace: "demo".to_string(), + agent: None, + timestamp: Utc.with_ymd_and_hms(2026, 5, 5, 5, 0, 0).unwrap(), + correlation_id: "run-1".to_string(), + data: Some(json!({"workflow_name": "verify"})), + }]; + + let summary = summarize_activation(&events); + assert!(!summary.activated); + assert!(summary.first_started_at.is_some()); + assert!(summary.first_completed_at.is_none()); + } } diff --git a/src/main.rs b/src/main.rs index c7012d9..cd9017e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -143,6 +143,7 @@ fn main() { ), Commands::Runs { command } => match command { RunsSubcommand::List => cli::runs::list(), + RunsSubcommand::Activation => cli::runs::activation(), RunsSubcommand::Show { ref run_id } => cli::runs::show(run_id), }, Commands::Verify {