19 — v0.9.0 Kernel Fixes: Dispatch Quality + Package Structure

PR branch: feat/v0.9.0-kernel-fixes
Commits: 8 (Tasks 1–7 + plan docs)
Tests: 365 passing (219 in test_synlynk.py + 146 across test_capability_scoring, test_e2e, test_instruction_reach, test_static_scan)


Where we were

The previous goalpost from PR #52 (v0.8.0 Support Engineer) was: "Support Engineer running stably in CI for at least one full week. Once confirmed, write .agents/pm.json for the PM Agent."

Before that week fully elapsed, AGY and Codex surfaced a higher-priority signal: six dispatch-quality defects that were undermining everything the agent loop was supposed to produce. Agents were getting over-broad context, unformatted prompts, and no verification contracts. Capability scores were gameable by trivial test suites. Identity signatures were wired in the schema but never populated. These weren't cosmetic — they were eroding the quality of every dispatch.

This PR addresses those six defects plus the package structure that makes future module-level work tractable.


Strategic shift

The roadmap called for a v0.9.3 async daemon and relay server. That remains the mid-term goal. But when AGY and Codex independently flagged these dispatch-quality gaps during code review, the decision was made to address them first. A relay built on top of weak dispatch primitives would accumulate debt fast.

The relay ownership model was also decided during this session: community-first hybrid. Community relay (relay.synlynk.com, Fly.io hosted) is the default experience for all users. Self-provisioned VPS (Hetzner/Fly.io) is gated behind --enterprise flag. This model is reflected in the updated project-docs/todo.md relay tasks and the docs/superpowers/specs/ roadmap realignment doc.


What shipped

Task 1: generate_context(scope="task:<story_id>") — scoped dispatch context

dispatch_agent was calling generate_context(scope="full") — meaning every agent dispatch got 7 days of devlog, all teammate activity, and the full memory file, most of it irrelevant to a single story. New: _generate_task_context(story_id) writes a minimal snapshot — story metadata, active tasks only, and up to 20 relevant source files filtered by engg_domain. Full context is still available for non-story dispatches.

def _generate_task_context(story_id: str) -> None:
    # queries DB for story metadata
    # writes: story header, active tasks from todo.md, filtered source skeleton
    # omits: devlogs, teammate activity, full memory

dispatch_agent now calls generate_context(scope=f"task:{story_id}" if story_id else "full").

Task 2: ## Relevant Files injection

New _relevant_files_for_story(story_id) queries the story's engg_domain and matches it against the scan cache skeleton. Up to 10 matching file paths are injected into every dispatch prompt as ## Relevant Files. Agents (especially Codex) perform significantly better when told which files to touch. If no scan cache exists or the story domain is "unknown", the section is omitted.

Task 3: ## How to Verify injection

New _verify_contract_for_story(story_id, task) derives a pytest invocation from the story title (lowercase + underscores, max 40 chars) and injects it as ## How to Verify at the end of every dispatch prompt — but only when a tests/ directory exists. This gives agents a concrete definition of "done."

## How to Verify
Run: `pytest tests/test_synlynk.py -k 'fix_auth_timeout' -v`
Expected: all matched tests pass, no new failures.

Task 4: Per-agent dispatch framing

New _format_prompt_for_agent(agent, context_text, story_id, task, file_section, verify_section) replaces the inline prompt construction in dispatch_agent. Three distinct formats:

  • Codex: leads with ## Task Criteria (task split into sentence-level bullets) + file list. Codex truncates/ignores narrative prose — leading with scannable criteria is measurably better.
  • AGY: leads with Task: <directive>, context truncated to 2000 chars. AGY passes the prompt via -p CLI arg; long prompts hit shell-escaping limits.
  • Claude (default): full context narrative + ## Your Task section, no changes from previous behavior.

Task 5: Ed25519 signing for capability ratings

The capability_ratings table has had an ed25519_sig TEXT column since the schema was written, but it was never populated. Now:

  • _ensure_identity_key() — creates ~/.synlynk/identity.key via ssh-keygen -t ed25519 if absent.
  • _sign_capability_rating(data) — signs a canonical json.dumps(data, sort_keys=True) payload via ssh-keygen -Y sign. Fail-safe: returns "" if key absent or signing errors. No test dependency on actual key presence.
  • _write_capability_rating now calls both before the INSERT and writes the signature to the DB.
  • synlynk identity init CLI subcommand added.

Dispatched to AGY — AGY executed the full TDD cycle: read the schema, added the functions before _best_agent_for_story, wired the signing, added the CLI subparser, and committed.

Task 6: Anti-gaming quality baseline

quality_auto previously scored test_pass_rate * 10 * 0.35 — a suite with 1 trivial passing assertion scored identically to 100 real tests. Fix:

  • _extract_auto_signals now returns "test_count" alongside "test_pass_rate".
  • _write_capability_rating caps quality_auto at 5.0 when test_pass_rate == 1.0 and test_count < 3.

A 47-test suite with 100% pass rate still scores ~9.5. A 1-test suite with 100% pass rate is capped at 5.0 — median, not trusted signal.

Dispatched to AGY (fell back to Claude subagent after AGY's first attempt lost working directory context).

Task 7: Package split — synlynk/ package

bin/synlynk.py (~4700 lines) is now a 5-line shim:

#!/usr/bin/env python3
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from synlynk import main
if __name__ == "__main__":
    main()

All application code lives in synlynk/__init__.py. Test imports updated across all 4 test files from sys.path.insert(0, '../bin') to sys.path.insert(0, '..').

Dispatched to Codex — Codex handled the mechanical copy, shim write, and import-path updates across all test files in one shot. 365 tests pass before and after.


Hybrid dispatch: the meta-story

This PR was built using synlynk's own dispatch mechanism to route tasks by agent competency:

Agent Tasks Rationale
Claude (Haiku) Tasks 1–3 Complex helper design, TDD setup
Claude (Sonnet) Task 4 Integration task (touches dispatch_agent internals)
AGY Tasks 5, 6 Targeted additions to specific functions
Codex Task 7 Mechanical file copy + path updates

This is the first PR where synlynk dispatched to AGY and Codex to build synlynk itself. AGY completed Task 5 cleanly. AGY's second attempt (Task 6) lost working directory context mid-run — the fallback to Claude caught this and completed correctly. Codex handled Task 7 in one clean shot.

The working-directory drift in AGY's Task 6 run is worth noting: dispatch_agent spawns the agent subprocess from the worktree CWD, but AGY can cd internally. Adding CWD pinning to the dispatch context is on the backlog.


Tests

  • tests/test_synlynk.py: 219 passing (up from 200 at PR start)
  • tests/test_capability_scoring.py: 47 passing (4 new from Task 6)
  • tests/test_e2e.py, test_instruction_reach.py, test_static_scan.py: 99 passing (unchanged)
  • Total: 365 passing

New goalpost

synlynk dispatch now produces agent-appropriate prompts with scoped context, file maps, and verification contracts. The scoring layer is anti-gaming hardened. The codebase is structured as a proper package.

Next: v0.9.1 — synlynk relay join community onboarding flow. The relay ownership model is decided (community-first); v0.9.1 ships the UX for it: synlynk relay join points at relay.synlynk.com with a graceful "launching at v1.0" message, and synlynk relay self-host --fly --enterprise is gated behind the enterprise flag.