Finding
Eighteen non-test files import directly from gremlins.executor.state without going through a Gremlin instance. This creates a second access path to state that bypasses whatever invariants Gremlin is meant to enforce.
Stages — drop State import, pass Gremlin explicitly
All stage files import State for their run(state) signature, but stages already hold a .gremlin attribute set via _set_gremlin_recursive. The magic assignment should be replaced by passing gremlin explicitly: Stage.run(gremlin) -> Outcome.
Stages don't mutate state — they produce artifacts and return an Outcome. The gremlin manages all state transitions (recording attempts, advancing scope, persisting to disk). Stages are pure-ish workers: given a gremlin for context, produce outputs. State becomes fully internal to Gremlin; stages never see it.
| File |
Currently imports |
gremlins/stages/base.py |
State |
gremlins/stages/composite.py |
State |
gremlins/stages/exec.py |
State |
gremlins/stages/loop.py |
State |
gremlins/stages/parallel.py |
State, StateData |
gremlins/stages/sequence.py |
State |
gremlins/stages/agent.py |
State |
gremlins/stages/agent_runner.py |
State |
gremlins/utils/proc.py |
State (TYPE_CHECKING only) |
Design: State as an attribute of Gremlin
State becomes gremlin.state, owned and managed by Gremlin. The runner calls stage.run(gremlin); stages read what they need from the gremlin (client, artifact_dir, repo, instructions, etc.) and return an Outcome.
Per-stage client overrides are handled in _collect_stages, which already iterates stages and resolves e.client or PACKAGE_DEFAULT. The resolved client (and stage_model) is patched onto gremlin.state inside the make_runner closure before calling stage.run(gremlin), keeping the override local to that stage's execution. Stages never see it directly.
Other callers — route through Gremlin
These files call build_state, resolve_artifact_dir, landable_shape, validate_gremlin_id, etc. directly. They should receive or construct a Gremlin and use its interface instead.
| File |
Currently imports |
gremlins/run_child.py |
State, StateData, build_state, validate_gremlin_id |
gremlins/spawn/child.py |
State, StateData, build_state, validate_gremlin_id |
gremlins/spawn/pipeline.py |
StateData, validate_gremlin_id |
gremlins/fleet/land.py |
landable_shape, resolve_artifact_dir, StateData, build_state |
gremlins/fleet/views.py |
StateData |
gremlins/executor/parallel_state.py |
StateData, resolve_state_file |
gremlins/executor/run.py |
StateData, resolve_artifact_dir, resolve_state_file |
gremlins/launcher.py |
StateData, validate_gremlin_id |
gremlins/cli/resume.py |
validate_gremlin_id |
Goal
gremlins.executor.state becomes an internal implementation detail of Gremlin. External callers hold Gremlin instances; stages take a gremlin argument and return an Outcome.
Finding
Eighteen non-test files import directly from
gremlins.executor.statewithout going through aGremlininstance. This creates a second access path to state that bypasses whatever invariantsGremlinis meant to enforce.Stages — drop
Stateimport, passGremlinexplicitlyAll stage files import
Statefor theirrun(state)signature, but stages already hold a.gremlinattribute set via_set_gremlin_recursive. The magic assignment should be replaced by passinggremlinexplicitly:Stage.run(gremlin) -> Outcome.Stages don't mutate state — they produce artifacts and return an
Outcome. The gremlin manages all state transitions (recording attempts, advancing scope, persisting to disk). Stages are pure-ish workers: given a gremlin for context, produce outputs.Statebecomes fully internal toGremlin; stages never see it.gremlins/stages/base.pyStategremlins/stages/composite.pyStategremlins/stages/exec.pyStategremlins/stages/loop.pyStategremlins/stages/parallel.pyState,StateDatagremlins/stages/sequence.pyStategremlins/stages/agent.pyStategremlins/stages/agent_runner.pyStategremlins/utils/proc.pyState(TYPE_CHECKING only)Design:
Stateas an attribute ofGremlinStatebecomesgremlin.state, owned and managed byGremlin. The runner callsstage.run(gremlin); stages read what they need from the gremlin (client, artifact_dir, repo, instructions, etc.) and return anOutcome.Per-stage client overrides are handled in
_collect_stages, which already iterates stages and resolvese.client or PACKAGE_DEFAULT. The resolved client (andstage_model) is patched ontogremlin.stateinside themake_runnerclosure before callingstage.run(gremlin), keeping the override local to that stage's execution. Stages never see it directly.Other callers — route through
GremlinThese files call
build_state,resolve_artifact_dir,landable_shape,validate_gremlin_id, etc. directly. They should receive or construct aGremlinand use its interface instead.gremlins/run_child.pyState,StateData,build_state,validate_gremlin_idgremlins/spawn/child.pyState,StateData,build_state,validate_gremlin_idgremlins/spawn/pipeline.pyStateData,validate_gremlin_idgremlins/fleet/land.pylandable_shape,resolve_artifact_dir,StateData,build_stategremlins/fleet/views.pyStateDatagremlins/executor/parallel_state.pyStateData,resolve_state_filegremlins/executor/run.pyStateData,resolve_artifact_dir,resolve_state_filegremlins/launcher.pyStateData,validate_gremlin_idgremlins/cli/resume.pyvalidate_gremlin_idGoal
gremlins.executor.statebecomes an internal implementation detail ofGremlin. External callers holdGremlininstances; stages take agremlinargument and return anOutcome.