Skip to main content

State

Struct State 

Source
#[non_exhaustive]
pub struct State {
Show 21 fields pub stage: Stage, pub phase: u32, pub agent: AgentKind, pub mode: Mode, pub gate_pending: bool, pub consecutive_failures: u32, pub infra_failures: u32, pub preflight_retries: u32, pub last_validate_failure_commit_count: Option<u32>, pub started_at: String, pub project_root: PathBuf, pub worktree_path: Option<PathBuf>, pub monitor_pid: Option<u32>, pub session_id: Option<String>, pub checkpoint_resumes: u32, pub stop_until: Option<Stage>, pub stopped: bool, pub stop_reason: Option<String>, pub yes_ship: bool, pub canary: Option<CanaryOutcome>, pub legacy_claude_launch: bool,
}
Expand description

Full workflow state persisted to .devflow/state.json.

§Construction

Marked #[non_exhaustive]: downstream crates must build this through State::new and then assign the fields they care about, rather than by struct literal. Deserialization is unaffected — the Deserialize derive and every #[serde(default)] field keep working exactly as before, so state files written by older binaries still load.

This exists because State accumulates a field roughly every phase that adds a run-scoped concept (worktree_path, monitor_pid, stop_until, yes_ship, and — in phase 28 — session_id and checkpoint_resumes). Without non_exhaustive, each of those additions is a semver-breaking change for any consumer that used a struct literal, which would force a major bump for what is really an internal bookkeeping change. Paying that cost once here makes every future field additive.

Fields (Non-exhaustive)§

This struct is marked as non-exhaustive
Non-exhaustive structs could have additional fields added in future. Therefore, non-exhaustive structs cannot be constructed in external crates using the traditional Struct { .. } syntax; cannot be matched against without a wildcard ..; and struct update syntax will not work.
§stage: Stage

Current workflow stage.

§phase: u32

Phase number being worked on.

§agent: AgentKind

Which coding agent was launched.

§mode: Mode

How the pipeline is driven (auto vs. supervise).

§gate_pending: bool

Whether a gate has been written and is awaiting a human response.

§consecutive_failures: u32

Consecutive Validate failures — drives the Auto-mode forced gate after crate::mode::MAX_CONSECUTIVE_FAILURES failures. Persisted across devflow advance invocations so the counter survives monitor restarts.

§infra_failures: u32

Consecutive infrastructure-class faults (ResourceKilled, AgentUnavailable) — distinct from Self::consecutive_failures (D-08, 17-01). Gates at crate::mode::MAX_INFRA_FAILURES. Any increment (wired in Plan 04) must use saturating_add so a long-running stuck loop cannot overflow u32. A serde-absent value (older persisted state) defaults to 0. Reset to 0 on every successful stage transition, alongside consecutive_failures (CR-01, 17-06 gap closure), so the ceiling bounds a stuck loop, not a phase’s lifetime.

§preflight_retries: u32

How many times a preflight gate has been resolved and retried for this phase (18f). Bounded by crate::mode::MAX_PREFLIGHT_RETRIES. Persisted rather than recursion-scoped because the documented wedge spanned separate devflow invocations after a monitor death — an in-process recursion-depth counter would reset to zero on every new process and fail to bound the exact incident it exists to prevent. Reset to 0 whenever preflight passes and whenever a human explicitly approves (GateAction::Advance), both inside run_preflight. Unlike Self::consecutive_failures and Self::infra_failures, this counter is NOT touched by transition().

§last_validate_failure_commit_count: Option<u32>

The commit count observed on the phase’s feature branch at the most recent Validate failure (999.66, D-03) — the forward-progress baseline crate::mode::consecutive_failures_made_progress compares against to decide whether a new failure begins a fresh streak or continues the existing one.

None means no prior failure has been recorded — either the first failure of a phase, or the first failure observed after resuming state written by a binary predating this field — and is deliberately distinct from Some(0), which means a failure WAS recorded and the branch genuinely carried zero commits at that moment; a later failure that again counts zero commits must accumulate against that Some(0) baseline rather than being treated as a fresh streak.

A serde-absent value (state written by a binary predating this field) deserializes to None, which is exactly the “no prior record” meaning above — the same backward-compat pattern as every other #[serde(default)] field added since 17-01.

Unlike Self::consecutive_failures and Self::infra_failures, this field is NOT touched by transition() — it is a baseline observation rather than a counter, matching how Self::preflight_retries and Self::checkpoint_resumes are handled. It is replaced wholesale at each failure rather than incremented, so it needs no saturating_add treatment, unlike every other numeric field on this struct.

§started_at: String

When the phase started (Unix seconds).

§project_root: PathBuf

Path to the project root.

§worktree_path: Option<PathBuf>

Working directory for the agent when running in a git worktree.

None means the agent runs in project_root. State and capture files always live under the main project_root; only the agent’s cwd changes.

§monitor_pid: Option<u32>

PID of the detached monitor process that owns the agent for the current stage, recorded by launch_stage at spawn time. None means no monitor has been spawned for this state yet, OR the state was written by a binary predating this field — in both cases the liveness probe reports Unknown, never Stuck.

§session_id: Option<String>

The Claude session id captured from the most recent captured stdout envelope for this phase’s current stage (D-04, 28-02), read via crate::agent_result::session_id_from_capture. None means EITHER “no session has been captured for this state yet” OR “the state was written by a binary predating this field” — both cases behave identically (no relaunch target to address). Recorded so a checkpoint auto-decide relaunch (plan 28-03) can --resume the exact session that hit the checkpoint rather than spawning a fresh one, which would lose the original session’s conversation context and permission mode.

§checkpoint_resumes: u32

How many times the current stage’s agent has been relaunched via a checkpoint auto-decide resume (D-04, 28-03). Bounds a stuck checkpoint loop against mode::MAX_CHECKPOINT_RESUMES (added in plan 28-03) the same way Self::infra_failures bounds an infra-fault loop against mode::MAX_INFRA_FAILURES. Reset to 0 by every ordinary fresh stage launch, so the ceiling bounds one stage’s resume budget, not a phase’s lifetime (the same distinction MAX_INFRA_FAILURES’ doc comment draws for infra_failures). Any increment must use saturating_add so a stuck loop cannot overflow u32. A serde-absent value (state written by a binary predating this field) defaults to 0.

§stop_until: Option<Stage>

The stage devflow start --until <stage> requests as the last stage to run before halting (20c). None means no stop point was requested (the pipeline runs to Ship), OR the state was written by a binary predating this field — both cases behave identically (no interception in transition()).

§stopped: bool

Set by transition() when stop_until names the stage just completed — a terminal-but-not-failed halt short of Ship (20c). false for a normal in-flight or completed-to-Ship phase, and for any state written by a binary predating this field.

§stop_reason: Option<String>

Human-readable reason recorded alongside stopped (20c). None when stopped is false, or when the state predates this field.

§yes_ship: bool

Pre-authorization for the Ship gate (D-04/D-05/D-06, 23-09), set only from the --yes-ship CLI flag typed on devflow start.

Persisted rather than passed through the call stack: the Ship gate fires inside a detached monitor’s advance process, minutes to hours after the launching devflow start process has already exited, so a CLI-scoped value would be gone by the time it matters — only a value written to state.json at start time survives to be read back by that later, separate process. false for any state written by a binary predating this field.

§canary: Option<CanaryOutcome>

What this run’s delivery canary established (D-13/D-15, 31-03), recorded by the first stage launch that routes through the Claude stream-json transport. None means EITHER “no canary has run for this run yet” OR “the state was written by a binary predating this field” — both cases behave identically: the canary runs.

Persisted rather than held in memory for the same reason Self::yes_ship is: each stage launch happens in a SEPARATE devflow process (the monitor’s own advance tail), so an in-process flag would reset to “not yet run” at every stage transition and re-spend a real throwaway agent invocation each time — which is exactly the symptom 31-RESEARCH Pitfall 5 names for a canary that landed in the per-stage preflight hook.

A recorded Absent/Unverified keeps refusing on every later launch in the run; it is not consumed by the first refusal.

§legacy_claude_launch: bool

D-11’s opt-out: force the pre-31 single-document Claude launch (positional prompt, --output-format json, the sh monitor) for this run, off by default.

false means EITHER “the operator did not ask for the legacy path” OR “the state was written by a binary predating this field” — both cases behave identically: the D-09/D-10 rollout decides the transport, which is the pre-existing behaviour.

Persisted rather than passed through the call stack for the reason Self::yes_ship gives: each stage launch happens in a SEPARATE devflow process (the detached monitor’s own advance tail), so a CLI-scoped value would be gone by the time the second stage launches and the run would silently revert to the stream transport mid-flight.

Only ever OR-ed, never cleared, once set — see pipeline_launch::apply_legacy_launch_opt_out. Clearing it on a plain devflow resume would be the same silent-drop class as stop_until’s old unconditional clear (999.60). To turn it back off, edit .devflow/state-NN.json or start a new run.

Implementations§

Source§

impl State

Source

pub fn new( phase: u32, agent: AgentKind, mode: Mode, project_root: PathBuf, ) -> Self

Create a new state for starting a phase at the Stage::Define stage.

Trait Implementations§

Source§

impl Clone for State

Source§

fn clone(&self) -> State

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for State

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<'de> Deserialize<'de> for State

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Serialize for State

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more

Auto Trait Implementations§

§

impl Freeze for State

§

impl RefUnwindSafe for State

§

impl Send for State

§

impl Sync for State

§

impl Unpin for State

§

impl UnsafeUnpin for State

§

impl UnwindSafe for State

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more