sea-last-mile
Enforces the 6-phase last-mile completion workflow after SEA-Forge codegen. Triggers after `just pipeline` to generate production-ready adapters (SQLAlchemy, nats-py, httpx, litellm) from manifests. Prevents hand-written adapter code in `src/gen/` directories and ensures gap-report, environment setup, integration tests, and semantic checks all pass.
SKILL.md
| Name | sea-last-mile |
| Description | Enforces the 6-phase last-mile completion workflow after SEA-Forge codegen. Triggers after `just pipeline` to generate production-ready adapters (SQLAlchemy, nats-py, httpx, litellm) from manifests. Prevents hand-written adapter code in `src/gen/` directories and ensures gap-report, environment setup, integration tests, and semantic checks all pass. |
name: sea-last-mile
description: Use when completing adapter generation after SEA-Forge pipeline/codegen. Applies when src/gen outputs are involved, enforcing generator-only edits, gap/last-mile validation, and CI parity checks including semantic fixture updates for generated diffs.
SEA-Forge Last-Mile Workflow Guardrail
Iron Rules
- Never hand-write files in
src/gen/directories. Adapter templates are generated from manifests bygen_adapter_template.pyandgen_test_template.py. - Handwritten adapters live outside
src/gen/. Both Python and TypeScript files outsidelibs/<CTX>/adapters/src/gen/are handwritten and must implement SDS port interfaces. - After
just pipeline <CTX>, always runjust last-mile <CTX>. The pipeline produces ports; last-mile produces the concrete adapters that implement those ports.
No-Technical-Debt Rule
This skill forbids placeholders of any kind. Do not introduce or suggest:
- TODO/FIXME/XXX/HACK/STUB markers
- Stubs, mocks-as-implementations, or empty handlers
raise NotImplementedError/throw new Error("not implemented")pass,..., or any placeholder logic
Exception: pass/... in @abstractmethod and .pyi stubs is permitted for contracts. Do not introduce raise NotImplementedError.
If an issue blocks progress, request explicit user feedback with options and a recommendation.
The Last-Mile Contract
┌─────────────────────────────────────────────────────────────┐
│ LAST-MILE CONTRACT │
├─────────────────────────────────────────────────────────────┤
│ 1. After `just pipeline <CTX>`, run `just last-mile <CTX>` │
│ 2. Adapters are generated from manifest + stack.yaml │
│ 3. No TODOs, stubs, or placeholders in final output │
│ 4. All 6 phases must pass before work is complete │
└─────────────────────────────────────────────────────────────┘
Trigger Conditions
This skill activates when:
- After Codegen: User ran
just pipeline <CTX>and needs adapters - Manual Adapter Attempt: Agent tries to hand-write a Python adapter in
src/gen/ - Gap Detected: Gap report shows missing adapters
- Gen Directory Edit: Agent tries to edit any file in
src/gen/directories - Missing Environment Variables: Adapter tests fail due to unconfigured
.env
Stop and Ask if a Generated Zone Is Targeted
If the request requires editing any generated file, stop and ask:
⚠️ **Implementation Gap Detected**
This request requires editing generated code, but:
- Generated outputs must be changed via specs and regeneration
- Handwritten edits in `src/gen/` are forbidden
**Options:**
1. **Spec-first path**: Update specs → `just pipeline <CTX>` → `just last-mile <CTX>` (recommended)
2. **Template fix**: If the generator template is wrong, fix
`tools/codegen/gen_adapter_template.py` and re-run (allowed — generator
tooling in `tools/codegen/` is editable)
3. **Clarify requirements**: Provide missing spec details
Which approach? I recommend option 1 unless the generator template itself is at fault.
Dual-Language Adapter Structure
SEA-Forge produces adapters in two languages. Know which zone you are in:
libs/<CTX>/adapters/
├── src/
│ ├── gen/ ← GENERATED (Python). Never hand-edit.
│ │ ├── __init__.py
│ │ ├── impl.py
│ │ ├── http/ ← Generated HTTP handlers
│ │ ├── sea_document_repository.py
│ │ ├── message_publisher.py
│ │ └── ...
│ ├── index.ts ← Manually authored (TypeScript). Allowed to edit.
│ ├── my-custom.adapter.ts ← Manually authored (TypeScript). Allowed to edit.
│ └── my-custom.adapter.spec.ts
├── tests/
│ └── integration/ ← Generated integration tests (testcontainers)
└── project.json
Rule: Python adapters in src/gen/ are generated. Files in src/ outside gen/ are manually authored (Python or TypeScript) and must implement port interfaces from the SDS.
Workflow: The 6 Phases
The just last-mile <CTX> recipe executes all 6 phases sequentially. You can also run phases individually.
Phase 1: Gap Analysis
Identify missing adapters, required packages, and environment variables:
just gap-report <CTX>
CLI flags (passed via just gap-report <CTX> <FLAGS>):
| Flag | Purpose |
|---|---|
--json | Machine-readable JSON output (used by Phase 2 for automation) |
--no-online | Skip PyPI/npm/crates.io version lookups (default in just last-mile) |
--populate-env | Write missing env vars to .env and .env.example |
--dry-run | Show what would be generated without writing files |
Review output for:
- Missing adapters (repositories, publishers, consumers, clients)
- Required Python packages not yet installed
- Environment variables needed by third-party services
Data sources: gap_report.py reads from docs/specs/<CTX>/<CTX>.manifest.json and docs/specs/shared/infrastructure/stack.yaml.
Phase 2: Generate Adapters
Generate all missing adapters and their integration tests:
# Full automated workflow (runs all 6 phases):
just last-mile <CTX>
# Or generate a single adapter manually:
just gen-adapter <CTX> <PORT_NAME>
just gen-adapter-test <CTX> <ADAPTER_NAME>
The automation loop in just last-mile:
- Queries
gap_report.py --jsonfor missing adapters - For each missing adapter, runs
gen_adapter_template.pyandgen_test_template.py - Both tools read the manifest to determine adapter pattern, port interface, and methods
Phase 3: Gap Verification
Re-run gap report to confirm all adapters were generated:
just gap-report <CTX> --no-online
If gaps remain after generation, the workflow exits with error. Investigate:
- Was the manifest updated since last
just pipeline? - Does the adapter pattern match a known template? (see Adapter Pattern Reference)
- Check
gen_adapter_template.pylogs for template errors.
Phase 4: Environment Setup
Populate .env and .env.example with required variables:
just gap-report <CTX> --populate-env --no-online
This phase scans the manifest for infrastructure dependencies (database URLs, API keys, NATS endpoints) and writes placeholder entries. It does not overwrite existing values.
Phase 5: Run Tests
Run adapter integration tests (requires Docker for testcontainers):
just test-adapters <CTX>
# Or all contexts:
just test-adapters-all
Tests are generated by gen_test_template.py using testcontainers (PostgreSQL, NATS, Redis) for realistic integration testing.
Phase 6: Semantic Validation
Run governance checks on the generated adapters:
just semantic-check <CTX>
This generates semantic fixtures and validates them. Depends on just semantic-fixtures-ctx <CTX> as a prerequisite (handled automatically).
If libs/<CTX>/adapters/src/gen/** changed in your branch, also add/update a non-generated fixture file under:
docs/specs/<CTX>/fixtures/semantic/*.fixture.yaml
Then verify CI parity:
GITHUB_BASE_REF=<base-branch> scripts/ci/spec_and_fixtures_guard.sh
Adapter Pattern Reference
| Pattern | Port Name | Third-Party Library | Generated Methods |
|---|---|---|---|
repository | *Repository | SQLAlchemy + asyncpg | save, get, list_all, delete |
outbox_publisher | MessagePublisher | nats-py (JetStream) | publish, initialize, close |
inbox_consumer | MessageConsumer | nats-py (JetStream) | consume, initialize, close |
opa_client | PolicyClient | httpx → OPA | evaluate, health_check |
pgvector_store | VectorStore | pgvector | save_embedding, search_similar |
sparql_client | GraphClient | httpx → Oxigraph | query, construct, update, ask |
llm_adapter | LLMProvider | litellm | complete_chat, generate_embedding |
flipt_client | FeatureFlags | httpx → Flipt | evaluate_boolean, evaluate_variant |
Every generated adapter includes:
- structlog structured logging (
logger = structlog.get_logger(__name__)) - OpenTelemetry tracing spans (
tracer = trace.get_tracer(__name__)) - Async/await patterns with connection pooling
- Dynamic import helpers for hyphenated directory names
- SQLAlchemy ORM models inline (for repository pattern)
- Retry/idempotency where applicable (outbox uses deduplication)
Concrete Example: semantic-core
The semantic-core context demonstrates the full adapter suite:
sea_document_repository.py— SQLAlchemy + asyncpg + OTel + structlogmessage_publisher.py— NATS JetStream with at-least-once deliveryflipt_client.py— httpx → Flipt with context-aware targetingllm_adapter.py— litellm with multi-provider support (OpenAI, Anthropic, Ollama)opa_client.py— httpx → OPA for policy evaluationvector_store.py— pgvector for embedding similarity searchast_node_repository.py,ir_node_repository.py,manifest_repository.py— SQLAlchemy repositories
Quick Commands
| Command | Purpose |
|---|---|
just gap-report <CTX> | Show missing adapters for a context |
just gap-report <CTX> --json | Machine-readable gap output |
just gap-report <CTX> --populate-env | Write missing env vars to .env |
just gap-report <CTX> --dry-run | Preview without writing |
just gap-report-all | Gap reports for all contexts with manifests |
just last-mile <CTX> | Full 6-phase workflow for one context |
just last-mile-all | Full workflow for all contexts |
just gen-adapter <CTX> <PORT> | Generate a single adapter template |
just gen-adapter-test <CTX> <ADAPTER> | Generate a single test template |
just test-adapters <CTX> | Run adapter integration tests (pytest) |
just test-adapters-all | Run adapter tests for all contexts |
just stack-validate | Validate stack.yaml YAML syntax |
just semantic-check <CTX> | Governance validation |
Definition of Done
Last-mile is complete when all of the following hold:
-
just gap-report <CTX>shows no missing adapters - All required Python packages installed and locked
- All environment variables present in
.envand.env.example - No TODO/FIXME/HACK/STUB markers in
libs/<CTX>/adapters/src/gen/ - No
NotImplementedErrorstubs -
just test-adapters <CTX>passes (Docker running) -
just semantic-check <CTX>passes - If
libs/<CTX>/adapters/src/gen/**changed, add/updatedocs/specs/<CTX>/fixtures/semantic/*.fixture.yaml -
GITHUB_BASE_REF=<base-branch> scripts/ci/spec_and_fixtures_guard.shpasses locally -
just cipasses (full CI, notci-quick)
Generator Tooling
The tools that generate adapters live in tools/codegen/ and are editable:
tools/codegen/gen_adapter_template.py— adapter code generationtools/codegen/gen_test_template.py— integration test generationtools/codegen/gap_report.py— missing adapter/test detection
These are tooling, not generated output. If an adapter template produces wrong output, fix the generator and re-run — never patch the generated output.
Troubleshooting
"Adapter already exists"
- If in
src/gen/(generated) — re-runjust last-mile <CTX>to regenerate from manifest - If in
src/outsidegen/(manually authored Python/TypeScript) — verify it implements the port interface; no action needed
"Tests failing"
- Verify Docker is running (
docker ps) — testcontainers need Docker - Check environment variables:
just gap-report <CTX> --populate-env - Verify third-party services are configured in
stack.yaml - Check pytest output:
just test-adapters <CTX>
"Gap report shows no adapters but gap exists"
- Verify manifest exists:
docs/specs/<CTX>/<CTX>.manifest.json - Check that aggregates are defined in the manifest with port references
- Run
just pipeline <CTX>first if manifest is missing or stale
"Online version lookup fails" (CI / air-gapped)
Use --no-online flag to skip PyPI/npm/crates.io lookups:
just gap-report <CTX> --no-online
The just last-mile recipe already uses --no-online by default.
"Dynamic import error for hyphenated directories"
Generated adapters include a _import_from_path helper for hyphenated directory names (e.g., libs/semantic-core/). If imports fail, verify the helper function is present at the top of the generated adapter and that sys.path includes the workspace root.
"Manifest not found"
The pipeline must run before last-mile:
just pipeline <CTX> # produces .manifest.json
just last-mile <CTX> # consumes .manifest.json
Resolving Bulk Missing Adapters
A common scenario after running just pipeline <CTX> on a new or expanded context is a gap report showing many missing adapters (10–20+ repositories plus messaging adapters). This section provides a systematic workflow for resolving them.
Recognizing the Problem
The gap report will show output like:
│ ⚠️ Missing Adapters:
│ - TemplateRegistryRepository → persistence.primary
│ Target: libs/documentation/adapters/src/gen/template_registry_repository.py
│ - ValidationResultRepository → persistence.primary
│ Target: libs/documentation/adapters/src/gen/validation_result_repository.py
│ - DocumentationJobRepository → persistence.primary
│ Target: libs/documentation/adapters/src/gen/documentation_job_repository.py
│ ... (14 more)
│ - MessagePublisher → messaging
│ Target: libs/documentation/adapters/src/gen/message_publisher.py
Why this happens: The gap report creates one AdapterGap per aggregate defined in the manifest's model.aggregates section, plus one MessagePublisher if outbox/messaging is enabled. After just pipeline, only ports and application handlers are generated — concrete adapter implementations require the separate last-mile phase.
Step-by-Step Resolution
1. Verify the manifest is current
Adapters are derived from the manifest. If the manifest is stale, regenerate it first:
just pipeline <CTX>
2. Check stack.yaml has required adapter_mappings
The gap report reads docs/specs/shared/infrastructure/stack.yaml for adapter pattern resolution. If adapter_mappings is missing or incomplete, generated adapters may use incorrect defaults.
just stack-validate
cat docs/specs/shared/infrastructure/stack.yaml | grep -A 10 adapter_mappings
Expected structure:
adapter_mappings:
Repository:
default: persistence.primary
pattern: repository
MessagePublisher:
default: messaging
pattern: outbox_publisher
3. Run last-mile to auto-generate all missing adapters
just last-mile <CTX>
This runs all 6 phases. Phase 2 iterates over missing_adapters from the JSON gap report and calls gen_adapter_template.py and gen_test_template.py for each. For 17 missing adapters, expect 34 file generations (adapter + test each).
4. If Phase 2 fails mid-generation
Some adapters may fail to generate while others succeed. Common causes:
| Symptom | Root Cause | Fix |
|---|---|---|
Port module not found | Port interface file missing or import path wrong | Re-run just pipeline <CTX> to regenerate ports |
Unknown pattern: <name> | stack.yaml missing the adapter_mapping for this port type | Add the mapping to stack.yaml |
Cannot resolve aggregate fields | Manifest aggregate has no fields key | Update the SDS/SEA spec to include entity fields, then just pipeline <CTX> |
Template error in gen_adapter_template.py | Jinja2 template bug or unhandled port shape | Fix tools/codegen/gen_adapter_template.py (editable tooling) |
FileExistsError | Adapter already generated from a previous partial run | Safe to ignore; re-run just last-mile <CTX> which skips existing files |
5. If Phase 3 (Gap Verification) still shows gaps
After generation, remaining gaps indicate adapters that could not be generated. Investigate individually:
# See exactly which adapters are still missing
just gap-report <CTX> --json --no-online | python3 -c "
import sys, json
data = json.load(sys.stdin)
for a in data.get('missing_adapters', []):
print(f\"{a['port_name']:40s} → {a['target_path']}\")
"
# Try generating a single adapter to see the error
just gen-adapter <CTX> <PORT_NAME>
6. Verify generated adapters are production-ready
After all adapters are generated, verify no debt markers slipped in:
rg -n "NotImplementedError|TODO|FIXME|HACK|STUB|pass$" \
libs/<CTX>/adapters/src/gen/ --type py
Expected: zero matches (except @abstractmethod in port interfaces, which live in libs/<CTX>/ports/, not adapters/src/gen/).
7. Run integration tests
just test-adapters <CTX>
If tests fail due to missing Docker containers, ensure Docker is running and testcontainers can pull images.
Anatomy of a Bulk Gap
Understanding the data flow helps debug large gaps:
manifest.json gap_report.py gen_adapter_template.py
┌─────────────────┐ ┌──────────────┐ ┌────────────────────┐
│ model.aggregates│──aggregate──→│ For each agg │──gap?───→ │ Read port module │
│ - Entity1 │ names │ check if │ YES │ Resolve fields │
│ - Entity2 │ │ <snake>_repo │ │ Pick template │
│ - ... │ │ .py exists │ │ (REPOSITORY/PUB/ │
│ │ │ in src/gen/ │ │ CONSUMER/etc.) │
├─────────────────┤ ├──────────────┤ │ Write adapter .py │
│ runtime. │──messaging──→│ check if │ │ Write test .py │
│ messaging. │ config │ message_ │ └────────────────────┘
│ outbox.enabled│ │ publisher.py │
└─────────────────┘ │ exists │
└──────────────┘
Key insight: one aggregate = one repository adapter. A context with 16 aggregates will report 16 missing repository adapters plus any messaging/client adapters.
Common Bulk-Gap Scenarios
Scenario A: Brand-new context (all adapters missing)
After first just pipeline <new-ctx>, all adapters are missing. This is normal.
just last-mile <new-ctx> # generates everything
Scenario B: Spec expanded (new aggregates added)
Adding aggregates to the SEA-DSL spec and regenerating the pipeline creates new ports but no adapters.
just pipeline <CTX> # regenerates manifest with new aggregates
just last-mile <CTX> # generates only MISSING adapters (existing ones are preserved)
Scenario C: Adapter generation succeeds but tests fail
- Check if port interface changed shape (new methods added by spec update)
- Regenerate the specific adapter and its test:
just gen-adapter <CTX> <PORT_NAME> just gen-adapter-test <CTX> <ADAPTER_NAME> - If the generated adapter shape is wrong, fix
tools/codegen/gen_adapter_template.py
Scenario D: Partial generation (some adapters fail, others succeed)
The just last-mile recipe uses || true for Phase 1 but set -euo pipefail overall. If Phase 2 fails mid-loop on one adapter, subsequent adapters are skipped.
Fix: Generate the failing adapter individually to see the error, fix it, then re-run last-mile:
just gen-adapter <CTX> <FAILING_PORT> # see the actual error
# fix the root cause (template, manifest, or stack.yaml)
just last-mile <CTX> # re-runs; skips already-generated adapters
Scenario E: Gap report misidentifies adapters as missing
The gap report checks for files at <adapter_root>/<snake_case_aggregate>_repository.py. If the aggregate name produces an unexpected snake_case conversion (e.g., HTTPClient → h_t_t_p_client_repository.py), the file may exist under a different name.
Fix: Check the actual filename vs. expected:
ls libs/<CTX>/adapters/src/gen/
just gap-report <CTX> --json --no-online | grep target_path
If the naming is wrong, fix to_snake_case() in tools/codegen/gap_report.py.
Decision Tree: Generated vs. Manually Authored Adapters
Is it a Python adapter implementing a port from the manifest?
├── YES → Generated in src/gen/ by `just last-mile`
│ Never hand-edit. Fix the generator template if wrong.
└── NO
Is it a TypeScript adapter implementing a port from the SDS?
├── YES → Manually authored in src/*.adapter.ts
│ Must implement the port interface.
│ Must have adjacent *.adapter.spec.ts test.
└── NO
Is it a generator tool (gen_adapter_template.py, etc.)?
├── YES → Editable tooling in tools/codegen/
└── NO → Not an adapter. Check sea-generator-first skill.
Related Skills
- sea-generator-first — Spec-first workflow guardrail (core entry point)
- sea-dsl-authoring — Writing SEA-DSL specifications
- sds-yaml — Creating machine-readable SDS YAML with port definitions
- spec-authoring — Creating ADR/PRD/SDS documents
- debugging-codegen-pipeline — When codegen or last-mile fails
- sea-generator-builder — Creating new generator templates
- governance-validation — Semantic check and CALM compliance