Skip to content
PortMCPPro

Porting a legacy codebase without losing a single symbol?
Port tracks every one, eight matchers, eleven verifiers, no LLM in the verification path.

Port turns cross-language migration from a heroic guessing game into machine-checkable progress. Every source symbol auto-correlated to its target across eight matching tiers. Eleven deterministic verifier checks per mapping, no model call, fully reproducible. Structural-hash drift detection so the source codebase moving underneath you never silently invalidates the port. Atomic per-symbol claims so five AI agents can port in parallel without ever claiming the same function. Cryptographically signed audit chain on every decision.

AI alone

  • Ports the next function. Forgets the other 47.
  • Claims completeness it cannot prove
  • Blind to the source codebase moving underneath
  • Two agents in parallel claim the same function
  • “Are we done?” answered by vibes
  • Six months later: no record of why a symbol was dropped

Twira Port powertool

  • Tracks every source symbol, auto-correlated to its target
  • Eleven deterministic verifier checks per mapping, no LLM
  • Structural-hash drift detection, stale mappings flagged automatically
  • Atomic per-symbol claims, agents never collide
  • Coverage is a machine-checkable percentage
  • Cryptographically signed audit trail on every decision

Your agent ports the next function. Port remembers the other forty-seven, and proves which ones are actually done.

init + sync

Register the source codebase and snapshot every symbol via tree-sitter.

match

Auto-correlate source → target across eight matching tiers.

discover

Workflow B, narrow to a named working set for task-scoped ports.

next + decide

Claim the next item atomically, then record the decision with rationale.

verify

Eleven deterministic checks per mapping. No LLM in the path.

You ask

You are porting a 38,000-symbol TypeScript codebase to Rust. Multi-week migration, five agents in parallel.

Twira instantly

  • port init --source ../legacy-app --alias legacy, registers the source, snapshots metadata
  • port sync --alias legacy, tree-sitter parses 38,127 symbols across 12,847 files
  • port match --alias legacy --tier 8 --confirm-large, 28,914 mappings proposed across 8 tiers
  • port bulk_triage --file-prefix vendor/ --status not_applicable, 2,840 excluded in one call
  • Five agents in parallel, each calls port next --claim agent-A (B / C / D / E) in a loop, atomic, no collisions
  • port verify --alias legacy --strict, 11 deterministic checks list every mapping that is not yet complete

Coverage is a number, not a vibe. 26,108 verified clean · 2,103 partial · 703 specific issues with file, line, and reason. Source drift caught automatically by structural hash. Then a few months from now: every decision is signed, every rationale is on the chain.

Without Twira
With Twira
Hallucinated completeness
Machine-checkable completeness
Forgotten symbols become bugs
Every symbol tracked
Source moves underneath silently
Structural-hash drift detection
Multi-agent collisions
Atomic per-symbol claims
No record of rejected approaches
Audited rationale on every decision
Verification via LLM (slow, fuzzy)
Verification via deterministic checks (sub-second, exact)

How the agent uses this

Agent calls port via MCP. Two workflows, do NOT mix. Workflow A (full-repo migration): init → sync → match → bulk_triage → next → decide → verify. Workflow B (task-scoped discovery): init → sync → discover → triage → lock → variants → verify. 16 actions total. Long-running: streams progress at each phase.

When you reach for it

  • Starting a serious cross-language port. twira port init --source ../legacy-app --alias legacy registers the source; port sync snapshots it; port match --tier 8 --confirm-large proposes mappings for every source symbol.
  • Splitting a big migration across multiple AI agents in parallel. Every agent calls twira port next --claim agent-N in a loop; the database guarantees no two agents ever get the same work item; total time scales linearly with the number of agents.
  • Building one specific feature in a new codebase from legacy code. port discover with a set name, a scope, keywords, and a depth finds every related symbol via four signals (file path, name match, call graph, shared DB tables); then triage, lock, variants, and verify work through the scoped set.
  • Knowing whether a port is actually done. port verify --strict runs 11 deterministic checks per mapping (no model in the path); the output is the catalogue of what still needs work, mapping by mapping.
  • Catching the source codebase moving underneath you. Drift detection automatically marks every mapping affected by source changes as stale; port next --strategy stale_first works through the stale ones first, so the port keeps pace with active source development.
  • Finding every place a shared resource is touched. "Every symbol that reads the users table" or "every handler that calls PAYMENT_GATEWAY_URL" is one query against the resources index, attributed to the enclosing symbol. Essential for safe schema migrations and shared-config refactors during port work.
  • Compliance evidence for a regulated migration. The audit chain provides the cryptographically signed record of who decided what, when, and why; the verifier provides the deterministic completeness proof.

See it work

$ twira port init --source ../legacy-app --alias legacy   # register
recon port sync --alias legacy                          # snapshot
recon port match --alias legacy --tier 8 --confirm-large  # auto-correlate
recon port bulk_triage --alias legacy --file-prefix vendor/ --status not_applicable
recon port next --alias legacy --claim agent-A           # claim work
recon port decide --mapping-id 4218 --decision-action map \
  --target-symbol User::serialize --rationale "shape + test name"
recon port verify --alias legacy --strict                # 11 deterministic checks
✓ initialised source "legacy" · 12,847 files · 38,127 symbols snapshotted✓ matched 28,914 mappings across 8 tiers (T1: 18,442 · T2: 4,103 · T3-8: 6,369)✓ 2,840 marked not_applicable via bulk_triage (vendor/ excluded)✓ claimed mapping #4218 (claimed_by: agent-A) source: auth/session.py::serializeUser · 4 callers · 2 tests candidate target: src/auth/session.rs::serialize (tier 2, confidence 0.92)✓ decision recorded · audit event #18,422 signed✓ verify --strict complete · 28,914 mappings checked in 4.2s ✓ 26,108 verified clean · ⚠ 2,103 partial · ✗ 703 issues ↳ 412 stubs (todo!/unimplemented!) · 187 missing caller wiring ↳ 64 stale (source moved since decision) · 40 test parity gaps Run twira port next --strategy stale_first to work through the stale ones first.

Two workflows. Do not mix them.

After running discover (Workflow B, task-scoped), do NOT run match (Workflow A, full-repo). Match processes the entire repo; discover narrows to a working set; running match after discover blows up the narrowed scope. Pick one workflow per source codebase at init time. The MCP tool description names this rule explicitly so your agent does not silently mix them.

Technical depth, for engineers who want it

In your editor

Your editor catches a missing import. It does not catch a missing port. The legacy function serializeUser is not visible in your new Rust codebase, it is just absent. Editors surface what is present; only structured tracking surfaces what is missing.

What Port does

Port is an MCP tool that runs cross-language migrations as a tracked workflow. Two workflows. A full-repo migration runs init → sync → match → bulk_triage → next → decide → verify. A task-scoped discovery runs init → sync → discover → triage → lock → variants → verify. Eight matching tiers cover everything from exact name lookup through normalised name (camelCase ↔ snake_case), doc-comment semantic overlap, path and module structure, type signature compatibility, test-name anchoring, call-graph callee overlap, and import-graph similarity. Eleven deterministic verifier checks then cover stub detection, parameter count match, return type presence, caller wiring, import graph, type signature compatibility, export wiring, test parity, workflow chain, sibling completeness, and imported-module completeness. Atomic per-symbol claims keep parallel agents from colliding. Structural-hash drift detection means a moving source codebase never silently invalidates the port.

How it actually works

Cross-language ports used to take a year and end with a graveyard of forgotten symbols. Every big refactor spawns dozens of "I think we got everything" moments that quietly become production bugs when some obscure caller turns out to have been depending on a function nobody noticed. Port turns the migration from a heroic guessing game into a tracked workflow. Every source symbol is accounted for, every target verified, every decision audited, every drift caught. Your AI agents do the implementing; Port makes sure they finish.

Modern AI agents are very good at writing the next function in a port. They are not so good at remembering that 47 other functions still need porting, that the function they just rewrote has six callers nobody updated, that the new version returns an Option where the original returned a nullable pointer, or that a sibling utility was silently never touched. Port is the deterministic data layer your agent leans on so it does not have to remember any of that. "What is next?" becomes a single call. "Is this port actually complete?" becomes eleven deterministic checks with no model in the path. "Did anything drift?" is a query against a structural-hash column. The agent stops hallucinating completeness because the answer is now machine-checkable.

There are two workflows, and you should pick one upfront. The first is a full-repo migration, when you are porting an entire source codebase. The matcher correlates every source symbol to its likely target across the whole tree, and you triage, decide, and verify through the queue. The second is task-scoped discovery, when you are building one specific feature in the new codebase by extracting the relevant legacy code. Discovery narrows the work to a named set, you triage and lock that set, then verify coverage within its scope. The tool description is explicit about the rule that protects you: once you have run discover, never run match. Discover narrows, match processes everything, and mixing them blows up the working set.

A full-repo migration runs init, then sync, then match. init registers the source project under an alias and snapshots its metadata, file count, languages, exclude rules. Sensible excludes are built in (vendor, node_modules, dist, build, lock files, minified bundles), and you can layer your own on top. sync walks the source tree, parses each file via tree-sitter, and writes the symbol snapshot. Every function, class, interface, and type is captured with its file, line range, signature, doc-comment, call graph, and import graph. match then auto-correlates source to target symbols across all eight matching tiers. From there you sit in the loop. bulk_triage drops known noise out of scope in one call. next hands you the next work item, atomically claimed. decide records what you did with it. verify runs the deterministic checks. Repeat until coverage is 100%.

Task-scoped discovery starts the same way. After init and sync, you call discover with a set name, a scope, a list of keywords, and a depth. The engine runs all four discovery signals at once and creates a named set with every candidate symbol it finds. From there the discover action serves several purposes via different flags: triage shows the pending candidates so you can review them and filter them by which signal found them; triage_signal lets you bulk-mark everything a single signal returned as relevant or not-applicable; lock freezes the working set so further discover calls cannot widen it. After that, variants looks for consolidation candidates inside the set, and verify checks coverage within the locked scope. Link the migration to a masterplan item via the port_scope tag and the work is tracked end-to-end.

The eight matching tiers run in order, stopping at the first confident match. The first tier is an exact name lookup in the target index. The second is a normalised name match, case-insensitive and aware of snake_case, camelCase, and PascalCase, so serializeUser correlates with serialize_user automatically. The third is doc-comment semantic overlap via embedding similarity, available when an embedding provider is configured. The fourth is path and module structure, so src/auth/session.ts correlates with src/auth/session.rs even when the symbols themselves diverged. The fifth is type signature compatibility, parameter count and return shape. The sixth is test-name anchoring, since test names that span both source and target are strong correlation evidence. The seventh is call-graph callee overlap, on the basis that two functions calling the same other functions are likely related. The eighth is import-graph similarity, on the same logic for module-level relationships. A tier parameter (1 to 8) caps how aggressive matching goes, the lower tiers are strict, the higher ones catch more with lower confidence. Pass dry_run to preview without writing.

Every source symbol carries one of ten statuses. There are five working states, unmapped (the default), auto_matched (the matcher proposed a target), mapped (a human or agent confirmed it), in_progress (being ported), and complete. Then there are five terminal escape hatches: improved (the target is better than the source approach), consolidated (merged into another mapping), eliminated (the feature was explicitly dropped in the port), not_applicable (out of scope), and deferred (later). Every status change writes an audit event with the actor, the rationale, and the prior state. A database-level CHECK constraint enforces that the five terminal statuses cannot be set without a decision_reason. Provenance is enforced at the schema level, not by hope. "Why did this 800-line function not get ported?" always has an answer.

A port is rarely binary, so Port also tracks six sub-statuses per mapping. The signature might be done but the core logic not started. The core logic might be ported but the error handling not addressed. Everything might compile but the callers in the target codebase have not been wired up. Tests might be ported and docs left stale. Each of these is its own boolean column, sig_complete, core_logic_complete, error_handling, callers_wired, tests_ported, docs_ported, and a half-ported mapping does not look the same as a fully-finished one in your coverage stats. The next agent on the task sees exactly what is still missing.

Verification is the heart of why Port is reliable. When you run verify, the engine runs eleven checks against the indexed data alone. No model call, fully reproducible, sub-second on most mappings. It looks for unfinished stubs in the target body (todo!, unimplemented!, panic!, and equivalents in other languages). It checks that the parameter count matches between source and target. It checks that if the source returns a value, the target does too. It checks caller wiring, that every caller in the target codebase actually reaches the new target. It checks the import graph, so the target’s dependencies are themselves ported and reachable. It checks full type signature compatibility, beyond just param count. It checks export wiring, so the target is actually exposed where the source was. It checks test parity, that tests for the source exist in the target. It checks the workflow chain, that the full call chain reaches end-to-end. In strict mode it also checks sibling completeness (other symbols in the same source file are ported) and imported-module completeness (the modules this symbol uses are themselves ported). Each check returns a structured issue with file, line, and reason. Deterministic, reproducible audit evidence.

Discovery itself runs four independent signals. The first is a path-prefix sweep, point it at src/pricing/ and it pulls every symbol under that path. The second is a name-keyword match against symbol identifiers. The third walks the call graph out from the seed symbols, configurable to one, two, or three hops in either direction. The fourth is the most useful: it finds every symbol that touches the same external resource as the seed set. All four run by default, with flags to disable individual signals when you want a narrower discovery.

That last signal, shared_resource, is the cleverest part of the tool. During sync, the engine parses every source file for external-resource patterns. It picks up SQL table references (any SELECT, INSERT, UPDATE, or DELETE against a named table), API route definitions (Express routes, FastAPI paths, generic REST patterns), and config-key access (config.get("X"), process.env.X, and framework-specific equivalents). Each detection is attributed to its enclosing symbol via line-range overlap with port_mappings, and recorded in port_resource_links with a resource_type that runs from db_table and api_endpoint to config_key, queue_topic, wire_format, and import_package. This is what makes "every symbol in the codebase that touches the users table" a single query. When you are extracting a feature into a new service, this signal catches the orphans your AI agent would have missed.

After a discovery, variants runs consolidation-candidate detection on the set. It looks for symbols with similar names, similar signatures, and similar resource-access patterns, the duplicates that accumulate in any large legacy codebase. The output is a list of candidates: "these four legacy functions all do roughly the same thing; you probably want one in the target." It is idempotent and safe to re-run as discovery widens. Variants do not auto-consolidate; they propose, you decide.

Drift detection is the feature that makes long-running ports survive. Cross-language migrations take weeks or months, and the source codebase keeps moving while you port from it. Port handles this with structural hashes baked into every mapping at decision time, both a full-content hash and two normalised AST hashes (source_struct_hash_s and source_struct_hash_c), the same kind of structural fingerprint that lets Diagnose suppressions survive refactors. When the source changes, every affected mapping is automatically marked is_stale, with provenance: stale_reason is either direct (the symbol itself changed) or transitive (something it depends on changed), and stale_depth records how many hops away the change was. Stale mappings show up in next (the stale_first ordering strategy prioritises them), in verify (where they appear as a distinct issue category), and in status. You never lose track of which parts of your port no longer reflect current source.

Multi-session coordination is what makes parallel porting actually work. Pass a claim string to next and the call atomically claims the work item. The mapping row updates claimed_by and claimed_at in the same transaction that returns the item, so another session calling next at the same instant gets a different item. Never the same one. Claims expire after a configurable timeout, so a crashed or idle session does not permanently block work. Split a 5,000-mapping migration across five sessions (or five frontier models via Team), each running twira port next in a loop with its own claim, and they will never collide. Total time scales linearly with the number of agents. Without atomic claiming this would be a coordination nightmare; with it, multi-agent porting is a single flag.

next supports three ordering strategies. The default, dependency_order, walks the graph topologically, callees first, callers last, so when an agent ports a caller, every function it calls is already done. priority walks by the per-mapping priority integer (0 to 100), which a human can set via decide --priority to pull a critical path forward. stale_first walks drifted mappings before fresh ones, useful when the source codebase has just had a big change and you want to rebase the port before continuing.

Port has a few safety rails to stop you blowing your own foot off. A match on a source with more than 10,000 symbols requires an explicit confirm_large flag, because the operation holds a database lock for several minutes and the guard prevents accidentally blocking other Port operations on the same DB. dry_run lets you preview a match without writing. The tier parameter caps matching aggressiveness, start strict at tier 4, ratchet up to tier 8 for the loose-match cleanup pass. bulk_triage takes a file-prefix and a status so you can exclude noise in batches. The built-in exclude defaults cover the obvious cases (node_modules, vendor, .git, dist, build, minified bundles, lock files, source maps). force is available on init for the rare case where you really do want to wipe the snapshot and restart.

All Port state lives in .Twira/index.db, across the sixteen-table port_* family. The source projects are described in port_sources, port_source_files, and port_modules. The main symbol-to-symbol mapping is port_mappings, with its sub-status and drift columns. The discovery layer is split between port_discovery_sets and port_discovery_members. Consolidation groups live in port_groups and port_group_members. Companion tracking for tests, configs, contracts, type mappings, and net-new symbols has its own tables (port_tests, port_config_files, port_contracts, port_type_mappings, port_net_new). Conflicts go to port_conflicts. Resource attribution goes to port_resource_links. And the audit chain itself lives in port_decisions.

Every decide action writes a row into port_decisions. The row records the actor (decided_by) and actor_type (human, AI, or policy), the rationale, the prior and new status, the source_hash and target_hash at decision time, the session_id, and a prev_hash linking it to the previous decision. That last field is the load-bearing one: it is the same Merkle-style chain the Audit tool uses, so your port history is tamper-evident. last_verified_at on each mapping records when verify last passed. A few months from now you can reconstruct exactly which agent (or which person) mapped serializeUser in auth.py to User::serialize in auth.rs, when, and why. Compliance loves this; future maintainers love it more.

Port is gated to Pro tier via the port_migration feature flag. The gate fires at the start of every MCP call into the tool, so a Free user cannot run any port action. Long migrations are exactly the workflow that needs the structured tracking Twira Pro provides.

Setup is a single command per source. twira port init --source ../legacy-app --alias legacy registers a source project. The first sync does the heavy lift, indexing the source codebase via tree-sitter; subsequent syncs are incremental. Multiple source projects can be registered alongside each other, useful when porting several legacy services into one consolidated target, and you pass the alias on every action to specify which one. No additional configuration is required; the snapshot lives in your existing .Twira/index.db.

One install. Your agent will know the difference in the first session.

$ curl -fsSL twira.com/install.sh | sh
Port, Tools · Twira