Status: Phase 1 / port abstractions in place — strangler-fig migration starts here.
The architecture audit (see chat transcript "PLF-1585 / koru-arch") identified that:
src/koru/has fan-out 537 — every refactor risks regressions in 3 unrelated places- god modules (
autonomous_cycle.py2148L,doctor.py1965L,context.py1269L) bounded_contexts/already exists but is a thin event-logger over the legacyautonomous_*.pycode pathextension.ts(1582L) is a god class on the plugin side too
CQRS alone does not address the coupling problem. Hexagonal (Ports & Adapters) does — and we already have the natural seam: the NDJSON socket protocol between the VSCode/Cursor plugin and the Python daemon.
┌──────────────────────────────────┐
│ Adapter (TS): plugins/koru- │
│ autopilot-vscode/extension.ts │ ← target: thin (~300L)
└────────────┬─────────────────────┘
│ NDJSON socket (already exists!)
┌────────────▼─────────────────────┐
│ Adapter (Py): koruide/client.py │ KoruIDEClient
│ + daemon/... │
└────────────┬─────────────────────┘
│ Ports ← THIS PoC
┌────────────▼─────────────────────┐
│ Application: koru.autonomous_*, │
│ koru.bounded_contexts.* │
└────────────┬─────────────────────┘
│
┌────────────▼─────────────────────┐
│ Domain: events, sagas, policies │
└──────────────────────────────────┘
src/koruide/ports.py:
IdeChatPort— outbound: paste text + submit (drive)IdeChatHistoryPort— inbound: observemessage.receivedfrom IDE-side LLMIdeLifecyclePort— health/shutdown of the daemon
All three are PEP 544 Protocols (structural typing) with @runtime_checkable
so existing concrete classes satisfy them without modification.
Verified by tests/test_koruide_ports.py:
KoruIDEClientalready implementsIdeChatPortandIdeLifecyclePort(no code change required to adopt)- A trivial in-memory
_FakeChatAdapteralso satisfiesIdeChatPort— application tests no longer need a Unix socket
Each ticket that touches koru.autonomous_* migrates a single call site:
- from koruide.client import KoruIDEClient
- def autopilot_step(text: str) -> bool:
- client = KoruIDEClient()
- return client.drive(text, submit=True).get("ok", False)
+ from koruide.ports import IdeChatPort, DriveOutcome
+ def autopilot_step(ide: IdeChatPort, text: str) -> DriveOutcome:
+ return ide.drive(text, submit=True)In production wire KoruIDEClient. In tests inject a fake. Over 10–20 tickets,
the call sites stop importing concrete classes; god modules shrink because
the IdeChat*Port parameter is the obvious split point.
- does not migrate any existing call site (zero churn) — by design
- does not add dependency injection container —
IdeChatPortis a function parameter, no framework needed - does not split
extension.ts— that iskoru-arch-2 - does not introduce Saga state machine — that is
koru-arch-3 - does not type events with Pydantic — that is
koru-arch-4
Pick a small caller in koru.autonomous_* (e.g. one helper that currently
does KoruIDEClient().drive(...)) and refactor it to take ide: IdeChatPort.
That is the first real strangler step — and it will already be testable
without spinning up the daemon.