# `orchestrator.sdlc.feature_runner`
<!-- generated by `orchestrator understand`; the PKG is source of truth, edits here are advisory -->

[← Episteme](../README.md) · [Architecture](../architecture.md)

**Source:** [`src/orchestrator/sdlc/feature_runner.py`](../../src/orchestrator/sdlc/feature_runner.py)

2 types · 21 functions · python

## Changing this safely

**Tested by** (3): `tests.plugin.test_server`, `tests.sdlc.test_autorun`, `tests.sdlc.test_feature_runner`

**Most depended-upon here** — a change to these reaches the most code (call graph, ≤4 hops):

- [`FeatureRunError`](../../src/orchestrator/sdlc/feature_runner.py#L49) — reaches **10** symbols
- [`_git_out`](../../src/orchestrator/sdlc/feature_runner.py#L516) — reaches **10** symbols
- [`_git`](../../src/orchestrator/sdlc/feature_runner.py#L511) — reaches **9** symbols
- [`_is_test_path`](../../src/orchestrator/sdlc/feature_runner.py#L506) — reaches **9** symbols
- [`FeatureRunResult`](../../src/orchestrator/sdlc/feature_runner.py#L66) — reaches **8** symbols
- [`_changed_files`](../../src/orchestrator/sdlc/feature_runner.py#L542) — reaches **8** symbols
- [`_changed_line_ranges`](../../src/orchestrator/sdlc/feature_runner.py#L242) — reaches **8** symbols
- [`_error_is_on_a_changed_line`](../../src/orchestrator/sdlc/feature_runner.py#L335) — reaches **8** symbols

_1 of the symbols other code depends on here have no test path the graph can see — worth a second look before changing them. Absence of a path is not proof of absence of a test: calls through an attribute chain (`obj.method()`) are skipped rather than guessed at, so indirect coverage is invisible._

## Types

### `FeatureRunError`

[`src/orchestrator/sdlc/feature_runner.py:49`](../../src/orchestrator/sdlc/feature_runner.py#L49)

- **Extends** (1): `RuntimeError`
- **Called by** (3 production · 3 test): [`_satisfy_the_ticket`](../../src/orchestrator/sdlc/feature_runner.py#L368), [`_validated_issue_key`](../../src/orchestrator/sdlc/feature_runner.py#L57), [`run_feature`](../../src/orchestrator/sdlc/feature_runner.py#L623), [`_boom`](../../tests/plugin/test_server.py#L312), [`test_a_failed_run_is_recorded_as_failed`](../../tests/sdlc/test_autorun.py#L336), [`test_a_failing_stage_stops_the_chain`](../../tests/sdlc/test_autorun.py#L207)
- **Fields**: `code`
- **Documented in**: `docs/specs/go-support-roadmap.md#packaging-changes`, `docs/specs/sandboxed-test-execution.md#wiring-sdlcfeature-runnerpy`

### `FeatureRunResult`

[`src/orchestrator/sdlc/feature_runner.py:66`](../../src/orchestrator/sdlc/feature_runner.py#L66)

- **Called by** (1 production · 2 test): [`run_feature`](../../src/orchestrator/sdlc/feature_runner.py#L623), [`_capture`](../../tests/plugin/test_server.py#L278), [`_fake_run`](../../tests/plugin/test_server.py#L253)
- **Fields**: `branch`, `codegen`, `files`, `grounding_chars`, `intent_id`, `issue_key`, `iterations`, `live`, `passed`, `pr_url`, `tests`, `title`, `worktree`
- **Documented in**: `docs/specs/sdlc-tracking-blueprint.md#11-implementation-roadmap-incremental-each-phase-shippable`, `docs/specs/sdlc-tracking-blueprint.md#3-what-to-measure-the-metric-model`, `docs/specs/sdlc-tracking-blueprint.md#6-attribution-model`, `docs/specs/sdlc-tracking-blueprint.md#7-what-exists-today-vs-the-gaps`, `docs/specs/sdlc-tracking-blueprint.md#8-storage-architecture`

## Functions

### `_changed_files`

[`src/orchestrator/sdlc/feature_runner.py:542`](../../src/orchestrator/sdlc/feature_runner.py#L542)

- **Called by** (1 production · 3 test): [`run_feature`](../../src/orchestrator/sdlc/feature_runner.py#L623), [`test_changed_files_excludes_build_output`](../../tests/sdlc/test_feature_runner.py#L509), [`test_changed_files_excludes_preexisting_repo_content`](../../tests/sdlc/test_feature_runner.py#L486), [`test_changed_files_falls_back_to_py_scan_when_not_git`](../../tests/sdlc/test_feature_runner.py#L504)
- **Calls** (1): `create_subprocess_exec`

### `_changed_line_ranges`

[`src/orchestrator/sdlc/feature_runner.py:242`](../../src/orchestrator/sdlc/feature_runner.py#L242)

- **Called by** (1 production · 1 test): [`_typecheck_the_change`](../../src/orchestrator/sdlc/feature_runner.py#L186), [`test_reading_the_diff_leaves_the_index_alone`](../../tests/sdlc/test_feature_runner.py#L1172)
- **Calls** (3): [`_untracked_python_files`](../../src/orchestrator/sdlc/feature_runner.py#L291), `create_subprocess_exec`, `search`

### `_error_is_on_a_changed_line`

[`src/orchestrator/sdlc/feature_runner.py:335`](../../src/orchestrator/sdlc/feature_runner.py#L335)

- **Called by** (1 production · 1 test): [`_typecheck_the_change`](../../src/orchestrator/sdlc/feature_runner.py#L186), [`test_only_errors_on_changed_lines_count`](../../tests/sdlc/test_feature_runner.py#L1068)
- **Calls** (1): `match`

### `_exec`

[`src/orchestrator/sdlc/feature_runner.py:234`](../../src/orchestrator/sdlc/feature_runner.py#L234)

- **Called by** (1): [`_typecheck_the_change`](../../src/orchestrator/sdlc/feature_runner.py#L186)
- **Calls** (1): `create_subprocess_exec`

### `_files_no_test_exercises`

[`src/orchestrator/sdlc/feature_runner.py:123`](../../src/orchestrator/sdlc/feature_runner.py#L123)

- **Called by** (1 production · 2 test): [`run_feature`](../../src/orchestrator/sdlc/feature_runner.py#L623), [`test_a_file_no_test_reaches_is_reported`](../../tests/sdlc/test_feature_runner.py#L1099), [`test_an_unprobed_file_is_not_reported_as_covered`](../../tests/sdlc/test_feature_runner.py#L1205)
- **Calls** (4): [`_git`](../../src/orchestrator/sdlc/feature_runner.py#L511), [`_git_out`](../../src/orchestrator/sdlc/feature_runner.py#L516), [`_is_test_path`](../../src/orchestrator/sdlc/feature_runner.py#L506), `pathlib.Path`
- **Documented in**: `docs/specs/graphir-sdlc-workflow.md#half-one-parallel-fan-out-measured-30ms-available-declined`

### `_git`

[`src/orchestrator/sdlc/feature_runner.py:511`](../../src/orchestrator/sdlc/feature_runner.py#L511)

- **Called by** (2): [`_files_no_test_exercises`](../../src/orchestrator/sdlc/feature_runner.py#L123), [`_prove_the_tests_test_something`](../../src/orchestrator/sdlc/feature_runner.py#L85)
- **Calls** (1): [`_git_out`](../../src/orchestrator/sdlc/feature_runner.py#L516)

### `_git_out`

[`src/orchestrator/sdlc/feature_runner.py:516`](../../src/orchestrator/sdlc/feature_runner.py#L516)

- **Called by** (3 production · 2 test): [`_files_no_test_exercises`](../../src/orchestrator/sdlc/feature_runner.py#L123), [`_git`](../../src/orchestrator/sdlc/feature_runner.py#L511), [`_prove_the_tests_test_something`](../../src/orchestrator/sdlc/feature_runner.py#L85), [`test_git_failures_say_why`](../../tests/sdlc/test_feature_runner.py#L1194), [`test_reading_the_diff_leaves_the_index_alone`](../../tests/sdlc/test_feature_runner.py#L1172)
- **Calls** (1): `create_subprocess_exec`

### `_is_test_path`

[`src/orchestrator/sdlc/feature_runner.py:506`](../../src/orchestrator/sdlc/feature_runner.py#L506)

- **Called by** (2 production · 1 test): [`_files_no_test_exercises`](../../src/orchestrator/sdlc/feature_runner.py#L123), [`_prove_the_tests_test_something`](../../src/orchestrator/sdlc/feature_runner.py#L85), [`test_test_paths_are_recognised`](../../tests/sdlc/test_feature_runner.py#L746)
- **Calls** (1): `pathlib.Path`

### `_is_typing_hygiene`

[`src/orchestrator/sdlc/feature_runner.py:330`](../../src/orchestrator/sdlc/feature_runner.py#L330)

- **Called by** (1 production · 1 test): [`_typecheck_the_change`](../../src/orchestrator/sdlc/feature_runner.py#L186), [`test_only_environment_codes_are_filtered`](../../tests/sdlc/test_feature_runner.py#L1047)
- **Calls** (1): `search`

### `_judge_against_the_criteria`

[`src/orchestrator/sdlc/feature_runner.py:342`](../../src/orchestrator/sdlc/feature_runner.py#L342)

- **Called by** (1): [`_satisfy_the_ticket`](../../src/orchestrator/sdlc/feature_runner.py#L368)
- **Calls** (1): [`SemanticReviewAdapter`](../../src/orchestrator/sdlc/review.py#L178)

### `_local_commit`

[`src/orchestrator/sdlc/feature_runner.py:533`](../../src/orchestrator/sdlc/feature_runner.py#L533)

- **Called by** (1): [`run_feature`](../../src/orchestrator/sdlc/feature_runner.py#L623)
- **Calls** (1): `create_subprocess_exec`

### `_prove_the_tests_test_something`

[`src/orchestrator/sdlc/feature_runner.py:85`](../../src/orchestrator/sdlc/feature_runner.py#L85)

- **Called by** (1): [`run_feature`](../../src/orchestrator/sdlc/feature_runner.py#L623)
- **Calls** (3): [`_git`](../../src/orchestrator/sdlc/feature_runner.py#L511), [`_git_out`](../../src/orchestrator/sdlc/feature_runner.py#L516), [`_is_test_path`](../../src/orchestrator/sdlc/feature_runner.py#L506)

### `_release_the_ticket`

[`src/orchestrator/sdlc/feature_runner.py:173`](../../src/orchestrator/sdlc/feature_runner.py#L173)

- **Called by** (1): [`run_feature`](../../src/orchestrator/sdlc/feature_runner.py#L623)

### `_repair_after_revision`

[`src/orchestrator/sdlc/feature_runner.py:468`](../../src/orchestrator/sdlc/feature_runner.py#L468)

- **Called by** (1): [`_satisfy_the_ticket`](../../src/orchestrator/sdlc/feature_runner.py#L368)
- **Calls** (2): `pathlib.Path`, [`run_with_autoheal`](../../src/orchestrator/sdlc/testenv.py#L470)

### `_resolve_language`

[`src/orchestrator/sdlc/feature_runner.py:597`](../../src/orchestrator/sdlc/feature_runner.py#L597)

- **Called by** (1 production · 1 test): [`run_feature`](../../src/orchestrator/sdlc/feature_runner.py#L623), [`test_resolve_language_detects_go`](../../tests/sdlc/test_feature_runner.py#L553)
- **Calls** (1): `from_repo`
- **Documented in**: `docs/specs/go-support-roadmap.md#where-go-already-is-today-it-is-half-wired`, `docs/specs/language-expansion-roadmap.md#the-fixed-recipe-per-language`, `docs/specs/typescript-codegen.md#design-multi-language-support-typescript`, `docs/specs/typescript-codegen.md#language-resolution`, `docs/specs/typescript-codegen.md#where-typescript-stands-today`

### `_satisfy_the_ticket`

[`src/orchestrator/sdlc/feature_runner.py:368`](../../src/orchestrator/sdlc/feature_runner.py#L368)

- **Called by** (1): [`run_feature`](../../src/orchestrator/sdlc/feature_runner.py#L623)
- **Calls** (5): [`FeatureRunError`](../../src/orchestrator/sdlc/feature_runner.py#L49), [`_judge_against_the_criteria`](../../src/orchestrator/sdlc/feature_runner.py#L342), [`_repair_after_revision`](../../src/orchestrator/sdlc/feature_runner.py#L468), `pathlib.Path`, [`run_with_autoheal`](../../src/orchestrator/sdlc/testenv.py#L470)

### `_typecheck_the_change`

[`src/orchestrator/sdlc/feature_runner.py:186`](../../src/orchestrator/sdlc/feature_runner.py#L186)

- **Called by** (1): [`run_feature`](../../src/orchestrator/sdlc/feature_runner.py#L623)
- **Calls** (4): [`_changed_line_ranges`](../../src/orchestrator/sdlc/feature_runner.py#L242), [`_error_is_on_a_changed_line`](../../src/orchestrator/sdlc/feature_runner.py#L335), [`_exec`](../../src/orchestrator/sdlc/feature_runner.py#L234), [`_is_typing_hygiene`](../../src/orchestrator/sdlc/feature_runner.py#L330)

### `_untracked_python_files`

[`src/orchestrator/sdlc/feature_runner.py:291`](../../src/orchestrator/sdlc/feature_runner.py#L291)

- **Called by** (1): [`_changed_line_ranges`](../../src/orchestrator/sdlc/feature_runner.py#L242)
- **Calls** (1): `create_subprocess_exec`

### `_validated_issue_key`

[`src/orchestrator/sdlc/feature_runner.py:57`](../../src/orchestrator/sdlc/feature_runner.py#L57)

- **Called by** (1): [`run_feature`](../../src/orchestrator/sdlc/feature_runner.py#L623)
- **Calls** (1): [`FeatureRunError`](../../src/orchestrator/sdlc/feature_runner.py#L49)

### `run_feature`

[`src/orchestrator/sdlc/feature_runner.py:623`](../../src/orchestrator/sdlc/feature_runner.py#L623)

- **Called by** (4 production · 45 test): [`_run_sdlc_feature`](../../src/orchestrator/cli.py#L1414), [`_runner`](../../src/orchestrator/cli.py#L1527), [`_stage_implement`](../../src/orchestrator/sdlc/autorun.py#L1025), [`sdlc_feature`](../../src/orchestrator/plugin/server.py#L88), [`test_a_change_that_does_not_satisfy_the_ticket_is_stopped`](../../tests/sdlc/test_feature_runner.py#L699), [`test_a_codegen_error_hands_the_ticket_back`](../../tests/sdlc/test_feature_runner.py#L1268), [`test_a_coverage_gap_asks_for_tests_not_an_implementation_change`](../../tests/sdlc/test_feature_runner.py#L1125), [`test_a_declined_gate_commits_nothing`](../../tests/sdlc/test_feature_runner.py#L935), [`test_a_failed_live_run_still_logs_what_it_spent`](../../tests/sdlc/test_feature_runner.py#L360), [`test_a_fully_met_verdict_still_ships`](../../tests/sdlc/test_feature_runner.py#L1316), [`test_a_judge_that_never_relents_stops_at_its_budget`](../../tests/sdlc/test_feature_runner.py#L787), [`test_a_live_run_moves_the_ticket_in_progress_before_writing_code`](../../tests/sdlc/test_feature_runner.py#L577), [`test_a_live_run_moves_the_ticket_to_in_review_when_the_pr_opens`](../../tests/sdlc/test_feature_runner.py#L633), [`test_a_live_run_with_an_injected_spec_reaches_the_pr`](../../tests/sdlc/test_feature_runner.py#L650), [`test_a_rejected_change_gets_a_chance_to_answer`](../../tests/sdlc/test_feature_runner.py#L769), [`test_a_revision_that_breaks_the_suite_is_repaired_and_ships`](../../tests/sdlc/test_feature_runner.py#L874), [`test_a_revision_that_breaks_the_tests_does_not_ship`](../../tests/sdlc/test_feature_runner.py#L804), [`test_a_safe_run_logs_nothing`](../../tests/sdlc/test_feature_runner.py#L380), [`test_a_safe_run_moves_nothing`](../../tests/sdlc/test_feature_runner.py#L596), [`test_a_satisfying_change_proceeds`](../../tests/sdlc/test_feature_runner.py#L722), [`test_a_type_error_on_a_changed_line_is_not_green`](../../tests/sdlc/test_feature_runner.py#L995), [`test_a_workflow_without_the_status_does_not_fail_the_run`](../../tests/sdlc/test_feature_runner.py#L606), [`test_a_worklog_failure_never_changes_the_verdict`](../../tests/sdlc/test_feature_runner.py#L391), [`test_adopted_issue_is_used_and_nothing_is_created`](../../tests/sdlc/test_feature_runner.py#L411), [`test_an_approved_gate_lets_the_run_finish`](../../tests/sdlc/test_feature_runner.py#L955), +24 more
- **Calls** (51): [`FeatureRunError`](../../src/orchestrator/sdlc/feature_runner.py#L49), [`FeatureRunResult`](../../src/orchestrator/sdlc/feature_runner.py#L66), [`GhPRAdapter`](../../src/orchestrator/sdlc/forge.py#L91), [`JiraAdapter`](../../src/orchestrator/intake/jira.py#L114), [`JiraConfig`](../../src/orchestrator/intake/jira.py#L80), [`LLMCodegenAdapter`](../../src/orchestrator/sdlc/codegen.py#L826), [`WorkspaceManager`](../../src/orchestrator/sdlc/workspace.py#L59), [`_changed_files`](../../src/orchestrator/sdlc/feature_runner.py#L542), [`_files_no_test_exercises`](../../src/orchestrator/sdlc/feature_runner.py#L123), [`_local_commit`](../../src/orchestrator/sdlc/feature_runner.py#L533), [`_prove_the_tests_test_something`](../../src/orchestrator/sdlc/feature_runner.py#L85), [`_release_the_ticket`](../../src/orchestrator/sdlc/feature_runner.py#L173), [`_resolve_language`](../../src/orchestrator/sdlc/feature_runner.py#L597), [`_satisfy_the_ticket`](../../src/orchestrator/sdlc/feature_runner.py#L368), [`_typecheck_the_change`](../../src/orchestrator/sdlc/feature_runner.py#L186), [`_validated_issue_key`](../../src/orchestrator/sdlc/feature_runner.py#L57), [`analyze_cached`](../../src/orchestrator/intake/cache.py#L189), [`backlog_path`](../../src/orchestrator/intake/backlog_doc.py#L21), [`build_service_for`](../../src/orchestrator/intake/factory.py#L272), [`compose_with_ontomesh`](../../src/orchestrator/spine/grounder.py#L111), [`detect_dotnet_tfm`](../../src/orchestrator/sdlc/testenv.py#L364), [`dotnet_toolchain_available`](../../src/orchestrator/sdlc/testenv.py#L336), `from_repo`, [`from_repo`](../../src/orchestrator/sdlc/grounding.py#L48), `getenv`, +26 more
- **Documented in**: `CHANGELOG.md`, `docs/specs/sdlc-tracking-blueprint.md#12-complete-automation-triggering-the-sdlc-without-a-human-kickoff`, `docs/specs/sdlc-tracking-blueprint.md#121-the-trigger-surface-events-work`, `docs/specs/sdlc-tracking-blueprint.md#125-automation-roadmap-layers-on-11`, `docs/specs/tri-repo-integration.md#phasing-ordered-by-usable-value-gated-on-the-spine`, `docs/specs/unified-ui.md#3-face-a-delegation-inbox-home`

### `unsupported_language_error`

[`src/orchestrator/sdlc/feature_runner.py:585`](../../src/orchestrator/sdlc/feature_runner.py#L585)

- **Called by** (1): [`sdlc_feature`](../../src/orchestrator/cli.py#L1275)

## Imports

`__future__.annotations`, `asyncio`, `collections.abc.Awaitable`, `collections.abc.Callable`, `dataclasses.dataclass`, `dataclasses.field`, [`orchestrator.catalog`](../../src/orchestrator/catalog/__init__.py#L1), [`orchestrator.catalog.profile`](../../src/orchestrator/catalog/profile.py#L1), [`orchestrator.core.env`](../../src/orchestrator/core/env.py#L1), [`orchestrator.core.llm`](../../src/orchestrator/core/llm/__init__.py#L1), [`orchestrator.intake.backlog_doc`](../../src/orchestrator/intake/backlog_doc.py#L1), [`orchestrator.intake.cache`](../../src/orchestrator/intake/cache.py#L1), [`orchestrator.intake.factory`](orchestrator.intake.factory.md), [`orchestrator.intake.jira`](../../src/orchestrator/intake/jira.py#L1), [`orchestrator.intake.service`](../../src/orchestrator/intake/service.py#L1), [`orchestrator.personas.software_engineer`](../../src/orchestrator/personas/software_engineer.py#L1), [`orchestrator.sdlc.codegen`](orchestrator.sdlc.codegen.md), [`orchestrator.sdlc.forge`](../../src/orchestrator/sdlc/forge.py#L1), [`orchestrator.sdlc.grounding`](../../src/orchestrator/sdlc/grounding.py#L1), [`orchestrator.sdlc.layout`](orchestrator.sdlc.layout.md), [`orchestrator.sdlc.review`](../../src/orchestrator/sdlc/review.py#L1), [`orchestrator.sdlc.scaffold`](orchestrator.sdlc.scaffold.md), [`orchestrator.sdlc.telemetry`](../../src/orchestrator/sdlc/telemetry.py#L1), [`orchestrator.sdlc.testenv`](orchestrator.sdlc.testenv.md), [`orchestrator.sdlc.testrunner`](orchestrator.sdlc.testrunner.md), [`orchestrator.sdlc.workspace`](../../src/orchestrator/sdlc/workspace.py#L1), [`orchestrator.spine.grounder`](../../src/orchestrator/spine/grounder.py#L1), `os`, `pathlib.Path`, `re`, `time`, `typing.Any`, `uuid`

## Imported by

[`orchestrator.cli`](orchestrator.cli.md), [`orchestrator.plugin.server`](orchestrator.plugin.server.md), [`orchestrator.sdlc.autorun`](orchestrator.sdlc.autorun.md), [`tests.plugin.test_server`](../../tests/plugin/test_server.py#L1), [`tests.sdlc.test_autorun`](../../tests/sdlc/test_autorun.py#L1), [`tests.sdlc.test_feature_runner`](../../tests/sdlc/test_feature_runner.py#L1)
