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-pCLI arg; long prompts hit shell-escaping limits. - Claude (default): full context narrative +
## Your Tasksection, 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.keyviassh-keygen -t ed25519if absent._sign_capability_rating(data)— signs a canonicaljson.dumps(data, sort_keys=True)payload viassh-keygen -Y sign. Fail-safe: returns""if key absent or signing errors. No test dependency on actual key presence._write_capability_ratingnow calls both before the INSERT and writes the signature to the DB.synlynk identity initCLI 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_signalsnow returns"test_count"alongside"test_pass_rate"._write_capability_ratingcapsquality_autoat 5.0 whentest_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.