--- title: "Semantic Port" description: "Continuously port upstream changes from one language to another using a ledger-driven loop" --- A semantic port workflow tracks commits in an upstream repository, analyzes each one for relevance, and either ports the change to a different codebase or acknowledges it as not applicable — then loops back for the next commit. It's an autonomous maintenance loop, not a one-shot build task. This pattern is useful when you maintain a downstream implementation (e.g., a Go SDK) that tracks an upstream reference (e.g., a Python SDK). Instead of manually reviewing every upstream commit, the workflow processes the backlog commit-by-commit, making intelligent port-or-skip decisions. ## The workflow Semantic Port workflow: Start → Fetch → Analyze → Plan → Implement → Validate → Tests pass? → Finalize → loops back to Fetch, with Skip shortcut from Analyze back to Fetch, Fix loop from gate back to Validate, and Done exit from Fetch ```dot title="semantic-port.fabro" digraph SemanticPort { graph [ goal="Port semantic changes from upstream Python repository to our Go implementation", rankdir=LR, default_max_retries=3, model_stylesheet=" * { model: claude-sonnet-4-5;} .hard { model: claude-opus-4-6; } .analyze { model: gemini-3.1-pro-preview;} " ] start [shape=Mdiamond, label="Start"] exit [shape=Msquare, label="Exit"] // Phase 1: Find the next unprocessed commit fetch [ label="Fetch & Identify", prompt="Find the next unprocessed upstream commit.\n\n\ 1. Run `python3 ledger/manage.py earliest` to get the oldest commit with status=new\n\ 2. If found, write the commit details to .fabro/current_commit.md and respond with:\n\ {\"preferred_next_label\": \"process\"}\n\ 3. If no new commits exist:\n\ a. Fetch latest from upstream: cd upstream/ && git fetch && git pull\n\ b. Find commits newer than the latest in ledger.tsv\n\ c. Add them with `python3 ledger/manage.py add `\n\ d. Try `earliest` again\n\ e. If still none, respond with: {\"preferred_next_label\": \"done\"}\n\n\ Respond with exactly one of: process or done." ] // Phase 2: Analyze the commit and decide port vs. skip analyze [ label="Analyze & Decide", class="analyze", prompt="Read .fabro/current_commit.md for the commit to process.\n\ Examine it with `git show ` in the upstream/ directory.\n\n\ Analyze the semantic changes — what functionality changed, not just syntax.\n\ Decide if this change is relevant to our Go implementation or if it is\n\ Python-specific, docs-only, or not applicable.\n\n\ Write .fabro/analysis.md with sections:\n\ - Commit summary\n\ - Semantic analysis\n\ - Decision: PORT or ACKNOWLEDGE (with reasoning)\n\ - Port plan (if porting): concrete tasks with file:line references\n\n\ If decision is ACKNOWLEDGE:\n\ 1. Update ledger: `python3 ledger/manage.py update acknowledged`\n\ 2. Commit: `git add ledger/ && git commit -m \"semport: acknowledge - \"`\n\ 3. Respond with: {\"preferred_next_label\": \"skip\"}\n\n\ If decision is PORT:\n\ Respond with: {\"preferred_next_label\": \"port\"}" ] // Phase 3: Refine the plan plan [ label="Finalize Plan", prompt="Read .fabro/analysis.md. Perform a final editorial pass.\n\ Write .fabro/plan.md ensuring each task has:\n\ - Concrete file:line references in our Go code\n\ - Clear acceptance criteria\n\ - Directly executable instructions\n\n\ Remove vague language. The plan must be actionable." ] // Phase 4: Implement the port implement [ label="Implement Port", class="hard", prompt="Follow the plan in .fabro/plan.md.\n\ Port the semantic changes to the Go codebase.\n\ Focus on semantic equivalence, not literal translation.\n\ Use Go idioms and respect existing architecture.\n\ Log all changes to .fabro/implementation_log.md." ] // Phase 5: Validate validate [ label="Validate", shape=parallelogram, script="cd go-sdk && go build ./... && go test ./... -v 2>&1 || true" ] gate [shape=diamond, label="Tests pass?"] // Phase 6: Fix failures fix [ label="Analyze & Fix", class="hard", max_visits=3, prompt="Tests or build failed. Read the test output from the prior stage.\n\ Read .fabro/plan.md and .fabro/implementation_log.md.\n\ Diagnose the root cause, fix the issue, and log the fix." ] // Phase 7: Update ledger and commit finalize [ label="Finalize", prompt="All tests pass. Finalize this port:\n\ 1. Update ledger: `python3 ledger/manage.py update implemented`\n\ 2. Commit all changes:\n\ `git add -A && git commit -m \"semport: implement - \"`\n\ 3. Write a brief summary to .fabro/implementation_summary.md" ] // Wiring start -> fetch fetch -> analyze [label="Process", condition="preferred_label=process"] fetch -> exit [label="Done"] analyze -> plan [label="Port", condition="preferred_label=port"] analyze -> fetch [label="Skip"] plan -> implement -> validate -> gate gate -> finalize [label="Pass", condition="outcome=succeeded"] gate -> fix [label="Fail"] fix -> validate finalize -> fetch } ``` ## Key patterns ### The commit-processing loop The core of this workflow is a loop: `fetch → analyze → (port or skip) → fetch`. Each iteration processes exactly one upstream commit, then loops back for the next. The loop terminates when `fetch` finds no more unprocessed commits and routes to `exit`. ``` fetch → analyze → [Skip] → fetch → analyze → [Port] → plan → implement → validate → gate → [Pass] → finalize → fetch → ... → [Done] → exit ``` This is fundamentally different from a build workflow that runs once and exits. The semantic port workflow is designed to process an entire backlog autonomously, handling dozens of commits in a single run. ### Ledger-driven state The workflow tracks disposition in an external ledger file (`ledger.tsv`) with three states: | Status | Meaning | |---|---| | `new` | Unprocessed — the workflow hasn't looked at this commit yet | | `acknowledged` | Reviewed and determined to be irrelevant (docs-only, Python-specific, etc.) | | `implemented` | Semantic changes ported to the Go codebase | The ledger is the source of truth for what's been processed. Because it's a plain file committed to Git, it survives across runs — you can stop and resume the workflow and it picks up where it left off. ### Semantic analysis, not literal translation The `analyze` node is the decision point. It examines each upstream commit for *what changed functionally*, not just what code was modified. A commit that refactors Python type hints has no semantic impact on a Go port. A commit that changes retry behavior in the HTTP client does. This distinction is critical — routing a different model (Gemini) to the analysis node via the `.analyze` class brings a fresh perspective to the port/skip decision: ``` .analyze { model: gemini-3.1-pro-preview;} ``` ### The fix loop When ported code fails tests, the workflow enters a bounded fix loop: ```dot gate -> fix [label="Fail"] fix -> validate ``` The `fix` node has `max_visits=3`, preventing infinite retry cycles. If the fix can't be resolved in 3 attempts, the run terminates rather than looping forever. ### Skip vs. port branching The `analyze` node uses [routing directives](/workflows/transitions#agent-transitions) to choose between two paths: ```dot analyze -> plan [label="Port", condition="preferred_label=port"] analyze -> fetch [label="Skip", condition="preferred_label=skip"] ``` When the agent decides a commit is irrelevant, it updates the ledger, commits the acknowledgment, and loops back to `fetch` immediately — no planning or implementation needed. This keeps the workflow efficient: trivial commits (typo fixes, CI config changes, docs updates) are processed in seconds. ## Multi-model routing The stylesheet assigns three tiers of models: ``` * { model: claude-sonnet-4-5; } // Default: plan, finalize .hard { model: claude-opus-4-6; } // Implementation, fixing .analyze { model: gemini-3.1-pro-preview; } // Analysis: fresh eyes ``` - **Sonnet** handles routine tasks: fetching commits, finalizing plans, updating the ledger - **Opus** handles the hard work: implementing ports and diagnosing test failures - **Gemini Pro** handles analysis: a different provider brings independent judgment to the port/skip decision, reducing the risk of a single model's blind spots ## Run configuration Pair the workflow with a run config TOML for repeatable execution: ```toml title="run.toml" _version = 1 [workflow] graph = "semport.fabro" [run] goal = "Port semantic changes from upstream openai-agents-python to our Go SDK" [run.model] name = "claude-sonnet-4-5" provider = "anthropic" [run.model.fallbacks] "claude-sonnet-4-5" = ["openai"] "gemini-3.1-pro-preview" = ["anthropic"] [[run.prepare.steps]] script = "git clone https://github.com/openai/openai-agents-python upstream || (cd upstream && git pull)" [[run.prepare.steps]] script = "pip install -r ledger/requirements.txt" [run.inputs] upstream_repo = "openai/openai-agents-python" downstream_lang = "go" ``` Launch with: ```bash fabro run semport.toml ``` ## Adapting this pattern The semantic port pattern generalizes beyond language porting: - **Spec tracking** — monitor an upstream specification (OpenAPI, protobuf) and propagate changes to client libraries - **Dependency updates** — process a queue of dependency version bumps, testing and committing each one - **Issue triage** — pull issues from a tracker, classify them, and route to the appropriate workflow - **Log analysis** — process a backlog of alerts or log entries, investigating each one The core structure is always the same: fetch the next item, analyze it, decide on an action, execute, record the disposition, loop. ## Further reading Edge conditions, routing directives, and how agents control flow. CSS-like rules for assigning models to workflow nodes. Retry policies, loop detection, and max_visits. TOML configs for repeatable, parameterized runs.