Skip to content

fix(stella-cli): carry the typed abort through the goal loop and lead turn to the terminal writers - #1893

Merged
macanderson merged 8 commits into
mainfrom
fix/1862-goal-loop-typed-outcome
Aug 6, 2026
Merged

fix(stella-cli): carry the typed abort through the goal loop and lead turn to the terminal writers#1893
macanderson merged 8 commits into
mainfrom
fix/1862-goal-loop-typed-outcome

Conversation

@macanderson

@macanderson macanderson commented Aug 6, 2026

Copy link
Copy Markdown
Owner

What & why

The goal loop (run_goal_cmd / run_goal_turn / run_goal_pipeline_turn) and the deck's lead turn (run_lead_turn / run_lead_pipeline_turn) answered with Result<(), String>, which has no room for the abort's typed AbortKind — so on their paths a deliberate stop (stuck-loop escalation, step cap, enforced budget) was indistinguishable from a crash by the time the terminal SESSIONS-registry status was written. A policy-stopped goal run recorded SessionStatus::Error, never Stopped, and exited 1 instead of 3.

This chases #1637's shape one level deeper, exactly along the seam #1862 specs:

Design exemplar: the same total-match projection module pattern agent/outcome.rs already established (and std's "constructors on the error type" shape for CliFailure::from_abort) — no new patterns invented.

Closes #1862

Refs #1826, #1653, #1637

The witness

  • This PR includes a witness test (fails on main, passes here)

Same family and justification as #1826's an_unsupervised_deliberate_stop_projects_stopped_not_error:

  • agent::outcome::tests::a_policy_stopped_goal_round_projects_stopped_not_error — the fold an aborted working round takes to the terminal registry write keeps AbortKind::DeliberateStop, and outcome_status projects Stopped; the Failure kind still projects Error. Fails on the old code the way SessionPresence::finish and the deck's exit writer still collapse a deliberate stop to a bool #1826's witness does: the projection did not exist, and the old fold stringified the status so the terminal write could only reconstruct CliFailure::errorError.
  • agent::outcome::tests::a_policy_stopped_raw_goal_loop_projects_stopped_not_error — the raw (--no-pipeline) half; the kind-less backstops (round cap) stay Error.
  • stella-core: goal::tests::session_budget_caps_total_spend_across_rounds now asserts the enforced-budget stop reaches Unmet as Some(AbortKind::DeliberateStop), and aborted_working_turn_ends_the_goal_loop asserts a provider failure reaches it as Some(AbortKind::Failure) — neither pattern compiles against the old kind-less enum.

The gate

  • cargo fmt --check (touched crates)
  • clippy -D warningsstella-cli + stella-core clean. Pre-existing break, not this PR's: stella-pipeline/src/pipeline/witness_stage.rs:55 fails clone_on_copy on the base branch and on main; open unbreak PRs fix(gate): unbreak main — clippy clone_on_copy, file-size baseline, gate-parity step count #1873 / fix(stella-pipeline): unbreak main — two clippy failures merged with #1813 #1859 own it. I verified my crates lint clean with that one line patched locally (patch not included — a peer PR owns the fix).
  • cargo test -p stella-core goal suite (18/18) and cargo test -p stella-cli --bin stella (1431/1431) — scoped per the 16GB-machine constraint; CI runs the full workspace.
  • RUSTDOCFLAGS="-D warnings" cargo doc -p stella-core -p stella-cli --no-deps clean
  • Docs: doc comments updated where the folds moved; no flags changed
  • CLA signed
  • Closes #1862 above and as a commit trailer

scripts/check-file-size.sh flags stella-protocol/src/event.rs (+2) and stella-tui/src/deck_render.rs (+3): both overages exist verbatim on the base branch and are named by unbreak PR #1873 — untouched here.

Nothing left behind

Summary by Sourcery

Preserve typed abort information through goal and lead execution paths so terminal session status and exit codes distinguish deliberate policy stops from crashes.

New Features:

  • Propagate AbortKind via GoalOutcome::Unmet and new CLI projections so policy-stopped goal runs now surface as "Stopped" with dedicated messaging instead of generic failures.

Bug Fixes:

  • Ensure goal runs and lead turns that stop due to policy or enforced budgets are recorded as SessionStatus::Stopped and exit with the correct non-error code instead of being treated as crashes.
  • Fix deck soft-stop handling so the session exit status and user-facing error events are derived from the shared outcome projection rather than raw strings.

Enhancements:

  • Standardize CLI failure handling for goal and lead flows by returning CliFailure instead of String and reusing shared outcome projection helpers.
  • Expose agent outcome helpers for reuse across goal and deck paths, consolidating terminal status decisions into a single outcome_status-based mechanism.
  • Extend goal-related tests in stella-core and stella-cli to assert that enforced budgets and provider failures carry the correct AbortKind through to session status projection.

Tests:

  • Add witness tests under agent::outcome validating that policy-stopped goal rounds and raw goal loops project SessionStatus::Stopped instead of Error.
  • Update stella-core goal tests to assert that budget caps and provider failures are reflected as GoalOutcome::Unmet with the appropriate AbortKind values.

Stella Test added 3 commits August 6, 2026 04:00
…sed writers too

PR #1828 (#1653) taught the supervised registry writers to record
SessionStatus::Stopped for a policy stop, but SessionPresence::finish and
its callers still collapsed the outcome to a bool on the way in — so an
UNSUPERVISED headless run (a pipe, CI, --foreground) that ended itself by
policy still aged into the SESSIONS overlay as Error, indistinguishable
from a crash, with no later supervised write to correct it.

Widen SessionPresence::finish (and one_shot_notification's wording) past
the bool: the caller now hands it the terminal SessionStatus, projected by
the one existing decider (daemon::outcome_status) via a fifth projection
in agent/outcome.rs, pipeline_session_status, so every writer reads a
deliberate stop (Stopped), an interrupt (Cancelled), and a crash (Error)
the same way. The goal loop still answers with a String that cannot carry
the abort kind, and the deck's session_exit reads the same stringly
run_lead_turn — both audited and filed as #1862.

Witness: outcome::tests::an_unsupervised_deliberate_stop_projects_stopped_not_error
(structural — the projection did not exist and the widened finish call
sites do not compile on the old signature, mirroring the #1653 witness).

Closes #1826
Refs #1653
Refs #1862
… turn to the terminal writers

The goal loop and the deck's lead turn answered with a `Result<(), String>`,
which has no room for the abort's typed `AbortKind` — so on their paths a
deliberate stop (stuck-loop escalation, step cap, enforced budget) was
indistinguishable from a crash by the time the terminal SESSIONS-registry
status was written: a policy-stopped goal run aged into the registry as
`Error`, never `Stopped`.

Chase #1637's shape one level deeper:

- `stella-core`: `GoalOutcome::Unmet` now carries the typed kind of the
  working turn's abort (`kind: Option<AbortKind>`; `None` for the backstops
  that are not turn aborts — round cap, unreachable verifier).
- `run_goal_cmd` / `run_goal_turn` / `run_goal_pipeline_turn` answer with
  `CliFailure` instead of `String`; the folds that stringified
  `PipelineStatus::Aborted` and `GoalOutcome::Unmet` are now the shared
  projections `agent::outcome::goal_round_break` / `goal_unmet_failure`,
  and the terminal write projects through `daemon::outcome_status` with the
  real failure rather than a reconstructed `CliFailure::error`. A stopped
  goal run also exits 3, per the exit-code taxonomy.
- The deck's `run_lead_turn` / `run_lead_pipeline_turn` answer with
  `CliFailure` through the existing `turn_outcome_result` /
  `pipeline_status_result` projections, and `session_exit` reads
  `outcome_status` — one decider for every terminal writer.

Closes #1862
Refs #1826, #1653, #1637

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @macanderson, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
stella-cli-docs Ignored Ignored Preview Aug 6, 2026 7:09pm

@sourcery-ai

sourcery-ai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Propagates typed AbortKind through goal and lead turn loops so deliberate stops are recorded as Stopped (and exit with the appropriate CliFailure code) instead of generic Error, by switching loop results to CliFailure, enriching GoalOutcome::Unmet, and centralizing projections in agent::outcome for all terminal writers.

Sequence diagram for goal run terminal status projection with typed AbortKind

sequenceDiagram
    actor User
    participant GoalCmd as run_goal_cmd
    participant GoalLoop as run_goal_turn
    participant OutcomeProj as agent_outcome
    participant Daemon as daemon_outcome_status
    participant Registry as SessionRegistry

    User->>GoalCmd: invoke goal command
    GoalCmd->>GoalLoop: run_goal_turn / run_goal_pipeline_turn
    GoalLoop-->>GoalCmd: Result<(), CliFailure>
    GoalCmd->>OutcomeProj: goal_round_break / goal_unmet_failure
    OutcomeProj-->>GoalCmd: CliFailure with AbortKind
    GoalCmd->>Daemon: outcome_status(Result<(), CliFailure>)
    Daemon-->>Registry: SessionStatus (Stopped | Error | Cancelled)
    Registry-->>User: recorded session outcome and exit code
Loading

File-Level Changes

Change Details Files
Carry typed AbortKind through goal loops and use CliFailure for goal command/turn results so terminal status and notifications distinguish policy stops from crashes.
  • Change run_goal_cmd, run_goal_turn, and run_goal_pipeline_turn to return Result<(), CliFailure> instead of Result<(), String>.
  • Introduce goal_round_break and goal_unmet_failure projections in agent::outcome to keep AbortKind when folding PipelineStatus and GoalOutcome into CliFailure.
  • Update goal loop pipeline logic to use goal_round_break, preserve abort kinds, and wrap verifier/pipeline errors in CliFailure::error instead of bare strings.
  • Adjust run_goal_cmd terminal presence.finish to feed the real CliFailure into daemon::outcome_status and to vary inbox notification text for deliberate stops vs failures.
crates/stella-cli/src/agent/goal.rs
crates/stella-cli/src/agent/outcome.rs
Ensure lead deck turns propagate CliFailure with typed aborts and share outcome projections with other terminal writers.
  • Change TurnEnd::Finished and run_lead_turn/run_lead_pipeline_turn signatures to use Result<(), CliFailure>.
  • Replace manual TurnOutcome and PipelineStatus stringification in lead paths with calls to outcome::turn_outcome_result and outcome::pipeline_status_result.
  • Update deck session exit handling to derive SessionStatus via daemon::outcome_status, and compare SOFT_STOP_REASON using CliFailure::message() and to_string().
  • Change authoring::record_and_reflect_turn to accept Result<(), CliFailure> so reflection continues to work with the new error type.
crates/stella-cli/src/command_deck.rs
crates/stella-cli/src/command_deck/authoring.rs
Enrich GoalOutcome::Unmet with optional AbortKind so the goal engine reports why the loop ended in a typed way, and add tests covering budget and provider failure cases.
  • Extend GoalOutcome::Unmet to include kind: Option and import AbortKind into goal.rs.
  • Populate kind as Some(kind) when a working TurnOutcome::Aborted ends the goal loop, and as None for round-cap and verifier-unavailable backstops.
  • Update goal tests to assert that enforced-budget stops carry AbortKind::DeliberateStop and provider failures carry AbortKind::Failure, which would not compile against the old enum.
  • Leave GoalOutcome consumers restricted to stella-cli (no external API change beyond kind being present).
crates/stella-core/src/goal.rs
Expose outcome projections to the deck and add witness tests to ensure deliberate stops project SessionStatus::Stopped while other failures remain Error.
  • Make agent::outcome::pipeline_status_result and turn_outcome_result pub(crate) and expose the outcome module itself as pub(crate) from agent.rs so deck code can reuse them.
  • Add tests a_policy_stopped_goal_round_projects_stopped_not_error and a_policy_stopped_raw_goal_loop_projects_stopped_not_error validating that AbortKind::DeliberateStop yields SessionStatus::Stopped while Failure and kind-less backstops yield Error via daemon::outcome_status.
  • Maintain existing message formats in projections so user-facing text is unchanged except for the deliberate-stop notification wording.
crates/stella-cli/src/agent/outcome.rs
crates/stella-cli/src/agent.rs

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Base automatically changed from fix/1826-unsupervised-stop-status to main August 6, 2026 19:05
@macanderson
macanderson merged commit 94dc88e into main Aug 6, 2026
2 of 3 checks passed
@macanderson
macanderson deleted the fix/1862-goal-loop-typed-outcome branch August 6, 2026 19:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The goal loop and the deck's lead turn answer with a String, so their terminal writers cannot record a policy stop

1 participant