Add comprehensive functional tests with mocked AI for CI readiness#6
Open
Add comprehensive functional tests with mocked AI for CI readiness#6
Conversation
- Add pytest-asyncio to dev deps with asyncio_mode='auto' in pyproject.toml - Add root tests/conftest.py with mock_agent_ai and agentfield_server_guard fixtures - Add functional tests: test_planner_pipeline.py, test_planner_execute.py - Add error-path tests: test_malformed_responses.py - Add isolation tests: test_node_id_isolation.py (16 tests, subprocess-based) - Fix existing fast/ tests: router wiring, verifier, cross-feature, integration tests - All 434 tests pass (1 skipped) with zero real API calls Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
swe_af.app.app.callboundary — zero real API calls, CI-safetests/fast/test files to match current interfaces (router wiring, verifier, cross-feature, integration)pytest-asyncio>=0.23to dev deps withasyncio_mode='auto'inpyproject.tomlChanges
pyproject.tomlpytest-asyncioto[dev]optional-deps; add[tool.pytest.ini_options]withasyncio_mode = "auto"tests/conftest.pymock_agent_ai(function-scoped) andagentfield_server_guard(session-scoped autouse) fixturestests/test_planner_pipeline.pyplan()reasoner returning realistic Pydantic-schema responsestests/test_planner_execute.pyexecute()reasoner with single-issue and external-action pathstests/test_malformed_responses.pytests/test_node_id_isolation.pyNODE_IDisolation between swe-planner and swe-fast modulesswe_af/fast/verifier.pytests/fast/test_*.py(5 files)Test plan
python -m pytest tests/ -x -qwithAGENTFIELD_SERVER=http://localhost:9999 NODE_ID=test-node— 434 pass, 1 skip, 0 real API callspython -m pytest tests/fast/ -x -q— 386 pass, 1 skip (fast/ suite)python -m unittest discover— 22 tests OK (existing unittest suite unaffected)make check— install + lint pass with no regressionsgrep -r 'agentfield\.io\|openai\.com\|anthropic\.com' tests/→ empty🤖 Built with AgentField SWE-AF
🔌 Powered by AgentField
📋 PRD (Product Requirements Document)
PRD: Multi-Repository Workspace Support
Date: 2026-02-18
Author: Product Manager (AI Agent)
Status: Final — Ready for Architecture & Engineering Execution
1. Problem Statement
SWE-AF's
build()function accepts exactly one repository viarepo_url: str(and companionrepo_path: str). All downstream stages — workspace setup, git operations, worktree management, prompt context, PR creation — are hard-coded to operate on a singlerepo_path. Teams shipping multi-service architectures (e.g., an API repo plus a shared-types library, a monorepo with per-service sub-projects, or a backend service alongside a dependent frontend) cannot use SWE-AF without pre-merging code or decomposing tasks manually.This PRD specifies the changes required to support N ≥ 1 repositories per build, while maintaining full backward compatibility for all existing single-repo callers.
2. Goal
Enable SWE-AF to accept a list of repositories with role metadata (primary vs. dependency), clone them efficiently in parallel, lay them out in predictable workspace directories, and propagate multi-repo awareness through every stage: planning prompts, coding prompts, git operations (commit/diff/PR per repo), and final PR output.
3. Success Metrics
build()acceptsrepos: list[RepoSpec]and produces correctBuildResultwhen N > 1.create_pr=Truereceives an independent draft PR.reposis omitted or has a single entry.4. Scope
4.1 Must-Have
RepoSpecdata model — New Pydantic model capturing per-repo metadata.build()parameter extension — Acceptrepos: list[RepoSpec] | Nonealongside legacyrepo_url/repo_path.list[RepoSpec]at entry; single-repo callers see no behavior change.asyncio.gather; usesasyncio.to_threadwrapping subprocess calls.WorkspaceManifestmodel — Runtime record of all mounted repos with absolute paths.BuildConfig&ExecutionConfig— Replace singlerepo_url: strfield withrepos: list[RepoSpec]; expose aprimary_repoproperty returning the designated primary.target_repointo coder task prompt; coder must commit to correct worktree root.run_git_initis called once per repo;GitInitResultextended to carryrepo_name.CoderResult.repo_namefield identifies which repo was modified; merger targets correct repo root.run_github_prinvoked once per repo that hascreate_pr=True; cross-repo PR cross-references added to each body.GitHubPRResultinBuildResult—pr_urlfield deprecated in favor ofpr_results: list[RepoPRResult];pr_urlproperty maintained as backward-compat alias.4.2 Nice-to-Have
RepoSpec.sparse_paths: list[str]enables shallow file-tree clones for large monorepos.~/.swe-af/git-cache/<host>/<org>/<repo>.gitacross builds for faster re-clones.BuildConfigoverrides — Override model, runtime, or PR settings on a per-repo basis.all_pr_resultsis provided.WorkspaceManifestserialized to.artifacts/plan/workspace.json— Human-readable record of what was cloned and where.4.3 Out-of-Scope
files_to_create/files_to_modify; no dedicated monorepo sub-path DAG logic required.package.json,Cargo.toml,pyproject.tomletc. to auto-discover repos; callers supply the repo list explicitly.main()CLI entry point remains unchanged;reposparameter is accessible only via programmaticbuild()calls andconfigdict.5. Data Model Specification
5.1
RepoSpec(new —swe_af/execution/schemas.py)Constraints (enforced in
model_validator):RepoSpecwithrole="primary"in anylist[RepoSpec].repo_urlmust matchr"^(https?://|git@|ssh://)[^\s]+".mount_pointmust not contain..or absolute path separators.5.2
WorkspaceRepo(new —swe_af/execution/schemas.py)5.3
WorkspaceManifest(new —swe_af/execution/schemas.py)5.4
RepoPRResult(new —swe_af/execution/schemas.py)5.5
BuildConfigchanges (existing —swe_af/execution/schemas.py)New field:
New property:
New
model_validator(mode="after"):reposis empty ANDrepo_urlis non-empty: synthesizerepos = [RepoSpec(repo_url=repo_url, role="primary")].reposis non-empty: setrepo_url = primary_repo.repo_urlfor legacy code paths.ValueErrorifreposis non-empty ANDrepo_urlis non-empty simultaneously (ambiguous input).ValueErrorif zero entries haverole="primary"after normalization.ValueErrorif more than one entry hasrole="primary".ValueErrorif duplicatemount_pointor duplicate derived repo names exist.5.6
BuildResultchanges (existing —swe_af/execution/schemas.py)NOTE: The existing
pr_url: str = ""field must be REMOVED and replaced by the property above pluspr_resultslist.5.7
DAGStatechanges (existing —swe_af/execution/schemas.py)5.8
PlannedIssuechanges (existing —swe_af/reasoners/schemas.py)5.9
CoderResultchanges (existing —swe_af/execution/schemas.py)5.10
GitInitResultchanges (existing —swe_af/execution/schemas.py)5.11
MergeResultchanges (existing —swe_af/execution/schemas.py)6. Workspace Setup
6.1 New Function:
_clone_reposFile:
swe_af/app.pyImplementation requirements:
asyncio.gather(*[asyncio.to_thread(_clone_single, spec, workspace_root) for spec in cfg.repos], return_exceptions=True).RuntimeError.workspace_rootis the parent ofspec.repo_path(no structural change from today).workspace_root = os.path.join(artifacts_dir, "workspace").target_pathper repo:spec.repo_pathif non-empty, elseos.path.join(workspace_root, mount_point_or_derived_name).6.2 Re-clone Handling
Extract existing re-clone logic into
_clean_existing_repo(repo_path: str, remote_url: str) -> Noneand call once per repo within_clone_single.6.3 Directory Layout
Single-repo builds: identical to current behavior —
repo_pathis used as-is.Multi-repo builds (N > 1):
7. Prompt System Updates
7.1 New Utility:
workspace_context_blockFile:
swe_af/prompts/_utils.py(new file)Output format (multi-repo):
7.2 PM Prompt (
swe_af/prompts/product_manager.py)pm_task_prompt()gainsworkspace_manifest: WorkspaceManifest | None = None(keyword-only).When non-None and len > 1, append
workspace_context_block(manifest)after repository context.Add directive: "When writing acceptance criteria that reference file paths, always prefix with the repository name (e.g.,
repo-name/path/to/file)."7.3 Architect Prompt (
swe_af/prompts/architect.py)architect_task_prompt()gainsworkspace_manifest: WorkspaceManifest | None = None(keyword-only).When multi-repo, append
workspace_context_block(manifest)and add:"Cross-repo components must reference files using absolute paths or repo-relative paths with repo name prefix. Define interface contracts that include the
target_repofield for each component."7.4 Sprint Planner Prompt (
swe_af/prompts/sprint_planner.py)sprint_planner_task_prompt()gainsworkspace_manifest: WorkspaceManifest | None = None(keyword-only).When multi-repo:
workspace_context_block(manifest).PlannedIssueMUST includetarget_reposet to the repo_name where changes land."depends_onrelationships."7.5 Coder Prompt (
swe_af/prompts/coder.py)coder_task_prompt()gainsworkspace_manifest: WorkspaceManifest | None = Noneandtarget_repo: str = ""(both keyword-only).When multi-repo, prepend to task context:
When single-repo: no change.
7.6 Verifier Prompt (
swe_af/prompts/verifier.py)verifier_task_prompt()gainsworkspace_manifest: WorkspaceManifest | None = None(keyword-only).When multi-repo, append
workspace_context_block(manifest)and add:"Acceptance criteria referencing files in dependency repos must be verified at the correct repo path."
7.7 GitHub PR Prompt (
swe_af/prompts/github_pr.py)github_pr_task_prompt()gainsall_pr_results: list[RepoPRResult] | None = None(keyword-only).When
all_pr_resultsis non-empty and contains successful PRs for other repos, append "Related PRs" section to generated PR body.8. DAG Execution Changes
8.1 Per-Repo Git Init
In
app.py, after_clone_repos(), callrun_git_initonce per repo concurrently. Collect results intoWorkspaceManifest.repos[i].git_init_result. Primary repo's result continues to populateDAGState.git_integration_branchetc.8.2 Per-Repo Worktree Scoping
workspace_setup_task_prompt(and by extensionrun_workspace_setup) gainsworkspace_manifest: WorkspaceManifest | None = None.Multi-repo worktree path pattern:
Branch name pattern:
Single-repo: existing patterns unchanged (no repo_name prefix).
8.3 Coding Loop — Repo-Scoped Git Operations
run_coding_looppassesworkspace_manifestandissue.get("target_repo", "")intocoder_task_prompt. Iftarget_repois empty, coder operates on primary repo (preserves backward compat).8.4 Merger — Per-Repo Branch Merging
run_mergerreceivesworkspace_manifest. Groupspending_merge_branchesbyrepo_nameand merges each group into the respective repo's integration branch. Each merge produces aMergeResultwithrepo_nameset.8.5 Integration Tester
run_integration_testerreceivesworkspace_manifest. Integration tests run from primary repo root by default; test commands may reference other repos via absolute paths.9. PR/Output Phase
9.1 Per-Repo PR Creation
In
app.py, after verification, iterateworkspace_manifest.reposwhererepo.create_pr=True. Callrun_github_pronce per repo. CollectRepoPRResultlist. Pass the growing list asall_pr_resultsto each subsequent call (enables cross-references).9.2 Plan Docs in PR
PRD and architecture
<details>sections are appended to the primary repo's PR body only.10. Backward Compatibility Contract
Specifically:
BuildResult.pr_urlreturns a string (not None, not a list)DAGStatefields unchanged in shape and meaningrepo_pathfor single-repo buildsMechanism:
BuildConfig.model_validatorsynthesizesrepos = [RepoSpec(repo_url=repo_url, ...)]whenreposis empty andrepo_urlis non-empty. For single-entryreposlists, workspace root is the existingrepo_path— not a new subdirectory.11. Interface Contracts
11.1
_clone_repos(new,swe_af/app.py)11.2
workspace_context_block(new,swe_af/prompts/_utils.py)11.3
workspace_setup_task_prompt(modified,swe_af/prompts/workspace.py)11.4
github_pr_task_prompt(modified,swe_af/prompts/github_pr.py)11.5
run_git_initrunner (modified,swe_af/prompts/git_init.pyand runner)The
git_init_task_promptgainsrepo_name: str = ""as keyword param, which is injected into the agent context and returned inGitInitResult.repo_name.12. Acceptance Criteria
All criteria are machine-verifiable. A QA agent validates each by running the specified command.
AC-01:
RepoSpecmodel validationAC-02:
BuildConfignormalizes legacyrepo_urltoreposAC-03:
BuildConfigrejects multiple primary reposAC-04:
BuildConfigrejects bothrepo_urlandreposset simultaneouslyAC-05:
BuildConfigsetsrepo_urlfrom primary in multi-repo modeAC-06:
WorkspaceManifestmodel construction and JSON serializationAC-07:
RepoPRResultmodel constructionAC-08:
BuildResult.pr_urlbackward-compat propertyAC-09:
DAGStatehasworkspace_manifestfield defaulting to NoneAC-10:
PlannedIssuehastarget_repofield defaulting to empty stringAC-11:
CoderResulthasrepo_namefield defaulting to empty stringAC-12:
GitInitResulthasrepo_namefield defaulting to empty stringAC-13:
MergeResulthasrepo_namefield defaulting to empty stringAC-14:
workspace_context_blockreturns empty string for single repoAC-15:
workspace_context_blockreturns table with all repos for multi-repoAC-16:
pm_task_promptacceptsworkspace_manifestparameterAC-17:
architect_task_promptacceptsworkspace_manifestparameterAC-18:
sprint_planner_task_promptinjectstarget_repoinstruction for multi-repoAC-19:
coder_task_promptinjects repo-scope block with correct path for target repoAC-20:
verifier_task_promptacceptsworkspace_manifestparameterAC-21:
workspace_setup_task_promptacceptsworkspace_manifestparameterAC-22:
github_pr_task_promptacceptsall_pr_resultsparameterAC-23:
_clone_reposis importable and has correct signatureAC-24:
BuildConfigrejects duplicate repo names / mount pointsAC-25: All existing tests pass without modification
13. Assumptions
git worktreeandgit sparse-checkout.asyncio.to_threadfor cloning — No native async git library introduced; subprocess-based clones wrapped in thread executor.workspace_rootdirectory.integration_branch,initial_commit_sha,git_modeinDAGStatecontinue to refer to primary repo's git state. Dependency repos' git state is tracked inWorkspaceManifestonly.BuildResult.pr_results.mount_pointuniqueness — Callers are responsible for uniquemount_pointvalues (or unique repo names); duplicate mount points raiseValueErrorinBuildConfigvalidator.sprint_planner_task_promptcalling convention — Existing positional args (goal, prd, architecture) are preserved;workspace_manifestis keyword-only. All existing callers pass arguments positionally or by name — adding a keyword-only parameter is non-breaking.github_pr_task_promptcalling convention —all_pr_resultsis keyword-only with defaultNone; existing callers need no changes.14. Risks
Risk: Coder agent commits to wrong repo despite prompt injection
Mitigation: Verifier runs
git statuson each repo; stray modifications will surface as unexpected changes. Issue advisor can recover via RETRY_APPROACH.Risk: Parallel clone failures partially initialize workspace
Mitigation:
_clone_reposusesasyncio.gather(..., return_exceptions=True); any exception triggers cleanup of all partially-cloned directories before raisingRuntimeError.Risk:
BuildConfignormalization silently wrong for edge casesMitigation: Validator explicitly raises
ValueErrorif bothrepo_urland non-emptyreposare set simultaneously. Documented in docstring.Risk: Sprint planner generates issues without
target_repoin multi-repo buildsMitigation: Prompt injection explicitly mandates
target_repo.PlannedIssue.target_repodefaults to""(primary), so even missing values degrade gracefully to primary-repo behavior.Risk:
pr_urlproperty break ifBuildResultis deserialized from old checkpointMitigation:
pr_resultsdefaults to[];pr_urlproperty handles empty list withreturn "". Old checkpoints withoutpr_resultsfield will deserialize to empty list via Pydantic defaults.Risk: Directory collision if two repos have same derived name
Mitigation:
BuildConfig.model_validatorchecks for duplicate derived repo names and raisesValueErrorbefore any cloning begins.Risk: Cross-repo merge conflicts (e.g., shared generated file tracked in two repos)
Mitigation: Sprint planner's "one issue per repo" constraint prevents cross-repo file ownership. This is a caller responsibility for truly shared files — out of scope for this PRD.
15. Implementation Dependency Graph
Level 0 — No dependencies (implement in parallel):
RepoSpecmodelWorkspaceRepomodelWorkspaceManifestmodelRepoPRResultmodelworkspace_context_block()utility (swe_af/prompts/_utils.py)Level 1 — Depends on Level 0:
BuildConfigextensions (repos,primary_repo, validator) — depends onRepoSpecGitInitResult.repo_namefieldMergeResult.repo_namefieldCoderResult.repo_namefieldPlannedIssue.target_repofieldDAGState.workspace_manifestfield — depends onWorkspaceManifestBuildResult.pr_results+pr_urlproperty — depends onRepoPRResultLevel 2 — Depends on Level 0 + Level 1:
_clone_repos()— depends onBuildConfig,WorkspaceManifestpm_task_promptextension — depends onworkspace_context_blockarchitect_task_promptextension — depends onworkspace_context_blocksprint_planner_task_promptextension — depends onworkspace_context_blockcoder_task_promptextension — depends onworkspace_context_blockverifier_task_promptextension — depends onworkspace_context_blockgithub_pr_task_promptextension — depends onRepoPRResultworkspace_setup_task_promptextension — depends onWorkspaceManifestLevel 3 — Depends on Level 2:
app.pybuild flow: replace single clone block with_clone_repos(), per-reporun_git_init, store manifest inDAGStaterepo_name, merge into correct repoapp.pyLevel 4 — Depends on Level 3:
build()flow with mocked gitEnd of PRD
🏗️ Architecture
Multi-Repo Workspace Architecture
SWE-AF Multi-Repo Support — Revision 3
Addresses all B-NEW-* blockers and C-NEW-* concerns from tech lead review.
Table of Contents
1. Overview
This document specifies all changes needed to enable SWE-AF to accept a list of
repositories (N ≥ 1) per build. The design extends the existing
schema/prompt/orchestration layers with zero behavior change for single-repo callers.
Core invariants:
repo_url/repo_pathcontinue to work identically.swe_af/execution/schemas.py(the canonical shared-typesmodule) so no circular imports occur.
workspace_manifestas a keyword-only parameter with a defaultof
None— callers that do not pass it get byte-for-byte identical output to today.swe_af/prompts/_utils.pyfor the sharedworkspace_context_blockhelper. All other changes go into existing files.2. Baseline Schema Reference
Existing types referenced by this document (unchanged, defined in
swe_af/execution/schemas.py):ExecutionConfigruntime,models,_resolved_models(PrivateAttr). Properties:ai_provider,git_model,merger_model,integration_tester_model(and all other role-model properties).BuildConfigruntime,models,repo_url: str = "",enable_github_pr,github_pr_base. Propertyai_providercalls_runtime_to_provider(self.runtime).model_config = ConfigDict(extra="forbid").DAGStaterepo_path,artifacts_dir,git_integration_branch,git_original_branch,git_initial_commit,git_mode,merged_branches,unmerged_branches,merge_results.IssueResultissue_name,outcome,result_summary,error_message,files_changed,branch_name,attempts,adaptations,debt_items.CoderResultfiles_changed,summary,complete,iteration_id,tests_passed,test_summary,codebase_learnings,agent_retro.GitInitResultmode,original_branch,integration_branch,initial_commit_sha,success,error_message,remote_url,remote_default_branch.MergeResultsuccess,merged_branches,failed_branches,conflict_resolutions,merge_commit_sha,pre_merge_sha,needs_integration_test,integration_test_rationale,summary.ExecutionConfigfield clarification (resolves B-NEW-2):ExecutionConfigis afully defined class in
swe_af/execution/schemas.py(lines 590–693 as of this writing).The fields used in
_merge_level_branchesand_init_all_reposare:config.merger_model— property returningself._model_for("merger_model")config.integration_tester_model— property returningself._model_for("integration_tester_model")config.ai_provider— property returning_runtime_to_provider(self.runtime)config.git_model— property returningself._model_for("git_model")No changes are made to
ExecutionConfig.ai_providerclarification:BuildConfig.ai_provideris an existing property(not a stored field), defined at line ~529 of
schemas.py. It is listed here forimplementer awareness; no changes needed.
3. New Pydantic Models
All models below are added to
swe_af/execution/schemas.py.3.1
_derive_repo_name(module-level private function)This consolidates the identical logic in
app.py's_repo_name_from_url(resolvesC-NEW-2). After this is added to
schemas.py,app.pyreplaces its localdefinition with:
3.2
RepoSpec3.3
WorkspaceRepo3.4
WorkspaceManifest3.5
RepoPRResult4. Existing Schema Extensions
All changes add new fields with defaults. No existing fields are renamed or removed.
4.1
BuildConfigExtensionsAdd to
swe_af/execution/schemas.py, insideBuildConfig:model_config = ConfigDict(extra="forbid")compatibility: Addingreposas adeclared field is safe —
extra="forbid"only rejects undeclared keys.osimport:schemas.pydoes not currently importos. Addimport osat the top.4.2
BuildResultExtensionsReplace
pr_url: str = ""with:pr_urlconstruction-site breakage: Any code that constructsBuildResult(pr_url='...')will receive aValidationErrorsincepr_urlis nolonger a field. Known sites that must be updated:
swe_af/app.py~line 508:BuildResult(..., pr_url=pr_url)→ usepr_results=[...]swe_af/fast/app.py: anyBuildResult(pr_url=...)construction → usepr_results=[...]BuildResult(pr_url=...)directly → usepr_results=[...]Read sites (unchanged):
result.pr_url,result.model_dump()["pr_url"],result_dict.get("pr_url")all continue to work.4.3
DAGStateExtensions4.4
CoderResultExtensions4.5
GitInitResultExtensions4.6
MergeResultExtensions4.7
IssueResultExtensions (resolves B-NEW-1)4.8
PlannedIssueExtensionsAdded to
swe_af/reasoners/schemas.py:5. Prompt Layer Changes
5.1 New File:
swe_af/prompts/_utils.py6. New Prompt Functions
The existing
*_prompts()functions returning(system_prompt, task_prompt)tuplesare unchanged. The new
*_task_prompt()functions addworkspace_manifestas akeyword-only parameter with default
None.6.1
pm_task_prompt—swe_af/prompts/product_manager.py6.2
architect_task_prompt—swe_af/prompts/architect.py6.3
sprint_planner_task_prompt—swe_af/prompts/sprint_planner.pyThis is the most critical multi-repo prompt. The function builds the full task prompt
string (equivalent to
sprint_planner_prompts()[1]) and appends multi-repo mandates.6.4
coder_task_prompt—swe_af/prompts/coder.pyThe existing
coder_task_promptsignature is extended with two new keyword parameters.All existing parameters and the entire existing function body are preserved.
6.5
verifier_task_prompt—swe_af/prompts/verifier.pyImports to add at top of verifier.py:
6.6
workspace_setup_task_prompt—swe_af/prompts/workspace.py6.7
github_pr_task_prompt—swe_af/prompts/github_pr.pyImport to add at top of github_pr.py:
6.8
__init__.pyUpdateAdd
workspace_context_blocktoswe_af/prompts/__init__.pyexports:7. app.py Changes
7.1 Imports
7.2
_clone_repos— New Async Function7.3
build()ModificationsThe following changes are made to the existing
build()function body, preserving allexisting logic for the single-repo path:
A. After
BuildConfigconstruction, before plan + git_init:B. Pass
workspace_manifesttoexecute():C. Per-repo PR creation loop (replaces existing single pr_url block):
NOTE on B-NEW-3 resolution: The previous architecture draft called
_make_legacy_ws_repo(repo_path, cfg)— a function that was never defined. Thisarchitecture eliminates that call entirely. The single-repo PR path continues to
use the existing
pr_urlstring variable directly, wrapping it inRepoPRResultat the end of the existing block.
D. Return
BuildResult:7.4
execute()Function Changesrun_dagsignature change:run_daggainsworkspace_manifest: dict | None = Noneparameter. Internally, immediately after
_init_dag_state()constructsdag_state,it assigns
dag_state.workspace_manifest = workspace_manifest.8. dag_executor.py Changes
8.1
run_dagSignature8.2
_init_all_repos— New Async Helpernote_fnavailability:_init_all_reposdoes not receivenote_fnin thissignature to keep it simple. If logging is needed, add
note_fn: Callable | None = Noneas an optional parameter and pass it from
run_dag.8.3
IssueResult.repo_namePropagation (resolves B-NEW-1)In
swe_af/execution/coding_loop.py, inrun_coding_loop(), when constructing thesuccess
IssueResult:In
swe_af/execution/dag_executor.py, in_execute_level(), after results are gathered:This ensures
_merge_level_branchescan safely readr.repo_namefrom anyIssueResultinlevel_result.completed.8.4 Per-Repo Merger Dispatch
_merge_level_branchesis augmented with a multi-repo dispatch path. The existingsingle-repo path is preserved unchanged:
9. Detailed Function Specifications
9.1 Module Dependency Graph
9.2 Acceptance Criterion → Code Mapping
RepoSpecin schemas.pyrolevalidated,create_pr=Truedefault,sparse_paths=[]defaultBuildConfig._normalize_reposrepo_urlsynthesizes singleRepoSpec(role='primary')BuildConfig._normalize_reposValueErrorBuildConfig._normalize_reposrepo_url + repossimultaneously →ValueErrorBuildConfig.repo_urlbackfillrepo_urlbackfilled intoself.repo_urlWorkspaceManifestin schemas.pyprimary_repo_name,model_dump_jsonround-tripRepoPRResultin schemas.pyrepo_name,success,pr_urlfieldsBuildResult.pr_urlpropertypr_urlor""DAGState.workspace_manifestNonePlannedIssue.target_repo""CoderResult.repo_name""GitInitResult.repo_name""MergeResult.repo_name""workspace_context_block""workspace_context_blockpm_task_promptsignatureworkspace_manifestparameterarchitect_task_promptsignatureworkspace_manifestparametersprint_planner_task_promptworkspace_manifestparam; multi-repo prompt contains'target_repo'coder_task_promptworkspace_manifest,target_repoparams;/tmp/libin outputverifier_task_promptsignatureworkspace_manifestparameterworkspace_setup_task_promptsignatureworkspace_manifestparametergithub_pr_task_promptsignatureall_pr_resultsparameter_clone_reposin app.pyasync def,cfg+artifacts_dirparamsBuildConfig._normalize_reposValueError10. Backward Compatibility
10.1 Single-Repo Callers
10.2
BuildResultSerialization10.3 Prompt Functions
All new parameters have
default=None. Callers that don't pass them get identicaloutput to today.
10.4 Test Compatibility
Tests that construct
BuildResult(pr_url='...')must change to usepr_results=[...].All other existing test patterns are unaffected.
11. Data Flow Examples
11.1 Multi-Repo Build
11.2 Single-Repo Build (Identical to Today)
12. Architectural Decisions
Decision 1:
_derive_repo_nameinschemas.py(resolves C-NEW-2)Chosen: Canonical function in
schemas.py. Imported intoapp.pyas alias.Rejected: Two functions with identical logic. They will diverge across releases.
Consequence:
schemas.pygainsimport re(not yet present). Onereimport isthe only addition to schemas.py's import block.
Decision 2:
WorkspaceRepo.model_config = ConfigDict(frozen=False)(resolves B-NEW-4)Chosen: Mutable model.
_init_all_reposassignsws_repo.git_init_resultin-place.Rejected: Rebuild manifest with updated repos list after
asyncio.gather. Morecomplex, requires re-constructing WorkspaceManifest, and the concurrent gather pattern
makes in-place mutation the natural fit.
Consequence:
WorkspaceRepoinstances can be mutated post-construction.Implementers should not pass them to functions that depend on immutability.
Decision 3:
workspace_manifestasdict | NoneinDAGState(resolves C-NEW-4)Chosen: Store as
WorkspaceManifest.model_dump(). Survives JSON checkpointserialization. Consumers reconstruct via
WorkspaceManifest(**dag_state.workspace_manifest).Rejected:
WorkspaceManifest | Nonefield. Would break checkpoint JSON serializationwithout custom encoders.
Consequence: Each consumer adds one reconstruction call. All seven checkpoint
round-trips (one per level) work out of the box.
Decision 4: Branch via
git rev-parseafter clone (resolves C-NEW-1)Chosen: Run
git -C {dest} rev-parse --abbrev-ref HEADin a thread after clone.Rejected:
spec.branch or 'main'fallback. 'main' would be wrong for repos with'master', 'develop', or other default branches.
Consequence: One extra subprocess per cloned repo. Cost is negligible (~5ms) vs.
clone time (seconds to minutes).
Decision 5:
IssueResult.repo_namefromCoderResult(resolves B-NEW-1)Chosen:
coding_loop.pypropagatescoder_result.repo_nametoIssueResult.repo_name.dag_executor.pybackfills fromissue['target_repo']whenCoderResult.repo_nameis empty.Rejected: Have
_merge_level_brancheslook uprepo_namefrom the issue dict by name.Issue dict lookup is fragile when names diverge from their dict representations.
Consequence: Two propagation points (coding_loop and dag_executor). The backfill
in dag_executor is the safety net when an exception-wrapped
IssueResultis createdwithout going through
run_coding_loop.Decision 6: One merger call per repo, concurrent (Architectural Decision 8)
Chosen:
asyncio.gatherover per-reporun_mergercalls in_merge_level_branches.Rejected: Single merger call handling all repos. Merger agent prompt assumes one
integration branch; extending it for multiple branches would require prompt redesign.
Consequence: N concurrent merger calls per level (N = repos with completed issues).
Total wall-clock time is bounded by the slowest merger, not sum of all mergers.
Decision 7: No new files except
swe_af/prompts/_utils.py(resolves parallel implementation safety)Chosen: All Pydantic types in
schemas.py, all prompt changes in existing files,one new utility file
_utils.py.Rejected: New module
swe_af/execution/multi_repo.py. Extra file means extra importchanges and more surface area for merge conflicts between parallel implementation agents.
Consequence:
schemas.pygrows ~150 lines.app.pygrows ~100 lines. Each promptfile grows ~10–30 lines. All growth is additive; no existing code is restructured.
Decision 8:
execute()gainsworkspace_manifestparameter (resolves C-NEW-4)Chosen:
execute()reasoner gainsworkspace_manifest: dict | None = None.build()passes
manifest.model_dump(). Insideexecute(),run_dag()receives it and assignsdag_state.workspace_manifest = workspace_manifestimmediately after_init_dag_state().Rejected: Pack manifest into
git_configdict.git_confighas a defined shape;appending unrelated data to it would be surprising coupling.
Consequence:
execute()andrun_dag()both gain one optional parameter withNonedefault. All existing callers are unaffected.Decision 9:
BuildResult.pr_urlas property withmodel_dumpinjectionChosen:
pr_urlis a@property.model_dumpis overridden to inject it.Rejected: Keep
pr_url: strfield alongsidepr_results. Two sources of truth forthe same value will diverge whenever
pr_resultsis updated without updatingpr_url.Consequence:
BuildResult(pr_url='...')construction raisesValidationError.Known construction sites must be updated (listed in §4.2).
13. Extension Points
The following capabilities are out of scope per the PRD but the architecture
accommodates them without schema changes:
RepoSpecwould gaingit_credentials_key: str = ""._clone_singleinjects env vars from a vault. No other changes.RepoSpec.sparse_pathsis already defined._clone_singlewould add
git sparse-checkout setafter clone.pyproject.toml/Cargo.tomlto populatecfg.repos.BuildConfig.reposis the stable intake.pr_resultsto coordinatetagging.
list[RepoPRResult]provides all PR URLs in one place._clone_singlewould add--recurse-submodulesflag if needed.Architecture Revision 3 — addresses B-NEW-1, B-NEW-2, B-NEW-3, B-NEW-4,
C-NEW-1, C-NEW-2, C-NEW-3, C-NEW-4, C-NEW-5 and all minor notes from tech lead review.