PR #42 — v0.6.0: Job Control + Model-Aware Capability Engine

The Broader Goal at the End of the Previous PR

After v0.4.0 (PR #39) shipped the Hybrid Workgroup Bootstrap — agent discovery, synlynk dispatch, job store, and the init wizard — the next milestone was the Capability Engine. The question was no longer "can synlynk dispatch agents?" but "can synlynk dispatch the right agent?" That required two things working together: a ledger of past performance (v0.5.0, PRs #41 and #44) and the job-control machinery to close the feedback loop at dispatch time (v0.6.0, this PR).

Strategic Shifts in This PR

Three issues surfaced during implementation that weren't in the original v0.6.0 design:

1. Tier resolution contamination (R2 critical bug). The first draft of _write_capability_rating() called extract_model_version(log_text, agent=agent) to get the completion model. When no # synlynk-meta header was present, this fell through to Tier 3 (the config default for that agent), then compared the config default against model_at_dispatch (live Tier 2 probe). On a normal single-model run where no header existed, split_model was incorrectly set to 1 — silently excluding the run from capability_scores aggregation. The R2 review surfaced this as a critical data-quality bug.

Fix: pass agent=None to extract only Tier 1, then resolve the full hierarchy explicitly: Tier 1 > Tier 2 > Tier 3. split_model is only flagged when both Tier 1 and Tier 2 are concretely known and differ.

2. Normalization omitted from the feature branch. Hotfix PR #44 applied quality_auto normalization (weighted_sum / total_weight) to main after v0.5.0 landed. This branch predated that merge. The old sum(scores) approach capped quality_auto at 6.5 when test_pass_rate was absent (only 0.65 of the weight was present). The fix was applied as part of the R2 response commit.

3. split_model inconsistency on attestation (R1). cmd_score_attest() set model_at_completion retroactively but didn't recalculate split_model. A run dispatched as gemini and attested as claude-opus remained split_model=0, letting it pollute capability scores. Fixed via SQL CASE expression in the UPDATE.

What This PR Ships

Capability scoring full design
Quality signals taxonomy

Four-Tier Model Version Resolution

_probe_model_version() fires a --version-style call to each agent CLI and scrapes the output using flexible regexes that handle formats like claude-3-5-sonnet (version prefix before family name). The result becomes model_at_dispatch on the job record. A # synlynk-meta block in the agent's output can override with Tier 1 (highest trust). Tier 3 (config default) fills the gap when neither is available — but Tier 3 never participates in split_model detection.

synlynk pr check

Hard-blocks merge (sys.exit(1)) if any capability_ratings row has model_version='unknown'. Acts as a pre-merge gate to ensure the ledger stays auditable.

synlynk score attest <story_id> <model>

Retroactively sets model_version and recalculates split_model for rows marked unknown — for cases where the agent CLI didn't emit a header and the probe failed.

Verifier Pipeline Capture

extract_verifier_meta() parses a structured # synlynk-meta block from verifier agent output into quality, correct, rework_needed, and verifier_model fields. When present, signal_source upgrades from 'auto' to 'verifier' and the verifier's quality score takes precedence.

org_domain_tags on Stories

Stories support a JSON org_domain_tags array for Tokq discoverability metadata. The tags are stored and surfaced but intentionally excluded from routing queries — routing uses only engg_domain × org_domain × industry, never secondary tags.

Clean Tier Hierarchy in _write_capability_rating()

After the R2 fix:

  • Tier 1 (# synlynk-meta header): authoritative, used as both model_at_completion and model_version
  • Tier 2 (model_at_dispatch, live probe): primary fallback — used as completion model when Tier 1 is absent
  • Tier 3 (config default): label of last resort, never used for split_model detection
  • split_model = 1 only when both Tier 1 and Tier 2 are non-unknown and differ

Tests

41 tests in tests/test_capability_scoring.py covering the full v0.6.0 surface: model probing, verifier parsing, pr check, score attest, dispatch routing, rework tracking, correct-column population, and the two new R2 regression tests that specifically guard against tier contamination and false-positive split_model flagging.

What This Achieves

v0.6.0 closes the capability feedback loop. Every completed dispatch now writes a structured, model-attributed, quality-scored row into capability_scores. The pr check gate keeps the ledger clean. The score attest command provides an escape hatch for CLI-only agents that can't emit headers. And the tier resolution hierarchy is now unambiguous: synlynk-meta header wins, live probe is the authoritative fallback, config default is the final label — and only when both live probe and header exist and differ does a run get flagged as split-model.

New Goalpost

With the capability ledger operational and routing wired, v0.7.0 targets the async pipeline and daemon: synlynk dispatch running as a persistent background process, a synlynk review TUI for live job monitoring, and the HTTP Context Server that will let remote agents pull project state without CLI invocation.