Background
src/compile/standalone_ir.rs is ~2,260 lines and the file name is a lie. Only ~30 lines (build_standalone_pipeline) are actually standalone-specific. The other ~2,000 lines define the canonical agentic pipeline shape (Setup → Agent → Detection → SafeOutputs → Teardown) and are called by all four targets via build_canonical_jobs (a pub(crate) builder extracted in 468359f6).
The most visible symptom is build_agentic_condition (lines ~970–1080), which composes the Agent-job condition from signals owned by multiple extensions. It reaches across abstraction boundaries to:
| Reference |
Real owner |
front_matter.is_synthetic_pr() / .pr_filters() / .pipeline_filters() |
types.rs |
step ID "synthPr" + its declared outputs |
extensions/ado_script.rs |
step IDs "prGate" / "pipelineGate" + their SHOULD_RUN output |
filter_ir.rs |
The typed-condition refactor in 660d2487 enforced those refs at compile time (graph validation rejects renames), but the knowledge of "which extensions feed the Agent-job condition and how" still lives in a compile target rather than in the extensions themselves.
The same shape concern applies to:
agent_job_variables_hoist (lines ~899–930) — knows about the synthPr step ID and its hoisted outputs.
wire_explicit_dependencies (lines ~1170–1210) — knows the canonical Setup → Agent → Detection → SafeOutputs edge list.
- Every
build_*_job (Setup / Agent / Detection / SafeOutputs / Teardown) — defines the canonical 5-job shape.
Why it matters
Every future cross-extension signal (e.g. a new extension that wants to gate the Agent job, or a different default Detection-job shape per target) ends up either:
- Carving another
Condition::Custom clause into build_agentic_condition, or
- Adding a new flag to
BuiltPipelineContext / StandaloneCtx and threading it through.
Both options add to the existing fan-in of standalone_ir.rs without giving extensions a place to own their own contributions to canonical-job conditions / variables / dependencies.
Proposed direction
Two related refactors. They could land separately:
1. Rename + split standalone_ir.rs
Extract a new module — src/compile/agentic_pipeline.rs (or canonical.rs) — that owns:
BuiltPipelineContext / build_pipeline_context / build_canonical_jobs
- All per-job builders (
build_setup_job, build_agent_job, build_detection_job, build_safe_outputs_job, build_teardown_job)
build_agentic_condition, agent_job_variables_hoist, wire_explicit_dependencies
- Helper step builders (
download_compiler_step, prepare_agent_prompt_step, etc.)
standalone_ir.rs shrinks back to its ~30-line shape wrapper. job_ir.rs / stage_ir.rs / onees_ir.rs import from agentic_pipeline directly instead of through a standalone_ir:: re-export.
This is a pure file move + import-path update — zero behaviour change.
2. Move condition contribution into Declarations
Today extensions contribute steps, env, hosts, MCPG entries, etc. — but not conditions on the canonical jobs they implicitly extend. Add a new field:
pub struct Declarations {
...
/// Clauses to AND into the canonical Agent job''s `condition:`.
/// The compiler folds these into a single `Condition::And` before
/// emitting. Lets an extension gate the Agent job declaratively
/// without the agentic-pipeline builder hard-coding knowledge of
/// each extension''s signals.
pub agent_conditions: Vec<Condition>,
// Possibly also:
// pub detection_conditions: Vec<Condition>,
// pub safe_outputs_conditions: Vec<Condition>,
}
Then:
AdoScriptExtension::declarations() emits:
- The synth-PR-skip clause (when
front_matter.is_synthetic_pr())
- The "real PR or synth-PR requires gate-passed" clause (when
pr_filters are active)
- The "ResourceTrigger requires pipeline-gate-passed" clause (when
pipeline_filters are active)
- User
expression: escape hatches in front matter become a typed Condition::Custom contribution from a small new code path (currently wired in build_agentic_condition).
build_agentic_condition reduces to Condition::And(decls.iter().flat_map(|d| d.agent_conditions).collect()).
After this, the agentic-pipeline builder no longer needs to know which extensions exist or what their step IDs are. New extensions can gate the Agent job without touching standalone_ir.rs.
Out of scope (for now)
- Same treatment for variable-hoist (
agent_job_variables_hoist). Today only AdoScriptExtension contributes; the threshold for adding a Declarations::agent_variables field is a second consumer.
- Custom per-job shapes (e.g. an extension that wants to insert a fifth canonical job). The current 5-job shape is hard-coded by design — the Three-Stage Pipeline Model is the security contract and shouldn''t be extensible.
Acceptance
src/compile/standalone_ir.rs is back to ~30 lines (only the standalone-specific shape wrapper).
- New
src/compile/agentic_pipeline.rs (or canonical.rs) owns the 5-job canonical shape.
Declarations carries agent_conditions: Vec<Condition> (and the canonical-jobs builder folds them).
build_agentic_condition deleted; all condition contributions come from extensions.
- Zero lock-file drift across all 33 fixtures.
- All existing tests pass; new tests cover the new
Declarations field and the per-extension condition contributions.
Background
src/compile/standalone_ir.rsis ~2,260 lines and the file name is a lie. Only ~30 lines (build_standalone_pipeline) are actually standalone-specific. The other ~2,000 lines define the canonical agentic pipeline shape (Setup → Agent → Detection → SafeOutputs → Teardown) and are called by all four targets viabuild_canonical_jobs(apub(crate)builder extracted in468359f6).The most visible symptom is
build_agentic_condition(lines ~970–1080), which composes the Agent-job condition from signals owned by multiple extensions. It reaches across abstraction boundaries to:front_matter.is_synthetic_pr()/.pr_filters()/.pipeline_filters()types.rs"synthPr"+ its declared outputsextensions/ado_script.rs"prGate"/"pipelineGate"+ theirSHOULD_RUNoutputfilter_ir.rsThe typed-condition refactor in
660d2487enforced those refs at compile time (graph validation rejects renames), but the knowledge of "which extensions feed the Agent-job condition and how" still lives in a compile target rather than in the extensions themselves.The same shape concern applies to:
agent_job_variables_hoist(lines ~899–930) — knows about thesynthPrstep ID and its hoisted outputs.wire_explicit_dependencies(lines ~1170–1210) — knows the canonical Setup → Agent → Detection → SafeOutputs edge list.build_*_job(Setup / Agent / Detection / SafeOutputs / Teardown) — defines the canonical 5-job shape.Why it matters
Every future cross-extension signal (e.g. a new extension that wants to gate the Agent job, or a different default Detection-job shape per target) ends up either:
Condition::Customclause intobuild_agentic_condition, orBuiltPipelineContext/StandaloneCtxand threading it through.Both options add to the existing fan-in of
standalone_ir.rswithout giving extensions a place to own their own contributions to canonical-job conditions / variables / dependencies.Proposed direction
Two related refactors. They could land separately:
1. Rename + split
standalone_ir.rsExtract a new module —
src/compile/agentic_pipeline.rs(orcanonical.rs) — that owns:BuiltPipelineContext/build_pipeline_context/build_canonical_jobsbuild_setup_job,build_agent_job,build_detection_job,build_safe_outputs_job,build_teardown_job)build_agentic_condition,agent_job_variables_hoist,wire_explicit_dependenciesdownload_compiler_step,prepare_agent_prompt_step, etc.)standalone_ir.rsshrinks back to its ~30-line shape wrapper.job_ir.rs/stage_ir.rs/onees_ir.rsimport fromagentic_pipelinedirectly instead of through astandalone_ir::re-export.This is a pure file move + import-path update — zero behaviour change.
2. Move condition contribution into
DeclarationsToday extensions contribute steps, env, hosts, MCPG entries, etc. — but not conditions on the canonical jobs they implicitly extend. Add a new field:
Then:
AdoScriptExtension::declarations()emits:front_matter.is_synthetic_pr())pr_filtersare active)pipeline_filtersare active)expression:escape hatches in front matter become a typedCondition::Customcontribution from a small new code path (currently wired inbuild_agentic_condition).build_agentic_conditionreduces toCondition::And(decls.iter().flat_map(|d| d.agent_conditions).collect()).After this, the agentic-pipeline builder no longer needs to know which extensions exist or what their step IDs are. New extensions can gate the Agent job without touching
standalone_ir.rs.Out of scope (for now)
agent_job_variables_hoist). Today onlyAdoScriptExtensioncontributes; the threshold for adding aDeclarations::agent_variablesfield is a second consumer.Acceptance
src/compile/standalone_ir.rsis back to ~30 lines (only the standalone-specific shape wrapper).src/compile/agentic_pipeline.rs(orcanonical.rs) owns the 5-job canonical shape.Declarationscarriesagent_conditions: Vec<Condition>(and the canonical-jobs builder folds them).build_agentic_conditiondeleted; all condition contributions come from extensions.Declarationsfield and the per-extension condition contributions.