MCP Governance Audit

{% if audit %}
{{ audit.gated_current }} / {{ audit.current_count }}
Tools Gated
{{ audit.status | upper }}
Audit Status
{{ audit.mutators_ungated_baseline }}
Mutators Ungated (ratchet target)
{{ audit.last_run | default('—') }}
Last Run
{% if has_drift %}
Drift findings — manual review
{% else %}

PASS No drift. Baseline {{ audit.baseline_count }} tools tracked.

{% endif %}
Baseline classification breakdown

From .context/audits/orchestrator-mcp-baseline.yaml. T-1166 deprecation annotated in YAML comments — facades that will be removed are not gating targets.

{% else %}
Audit not yet run. bash agents/audit/orchestrator-mcp-scan.sh or bin/fw audit --section orchestrator.
{% endif %}

Live Sessions ({{ sessions_total }})

{% if sessions_err %}
{{ sessions_err }}

Make sure the TermLink hub is running locally. Routing/orchestration data is live-only; nothing is cached on this page.

{% elif sessions_total == 0 %}
No TermLink sessions registered.
{% else %}

By task-type

{% if task_type_counts %} {% for tt, n in task_type_counts %} {% endfor %}
task-typecount
{{ tt }}{{ n }}
{% else %}
No sessions tagged task-type:<X>.

This is the symptom — orchestrator can route by task-type, but {{ untagged_sessions }} live session(s) carry no such tag. Tracked under T-1643 (Arc B — framework wiring).

{% endif %}

By role

{% if role_counts %} {% for r, n in role_counts %} {% endfor %}
rolecount
{{ r }}{{ n }}
{% else %}
No sessions tagged role:<X>.
{% endif %}
Session table ({{ session_rows | length }}{% if session_rows_truncated %} of {{ sessions_total }}, top 50 shown{% endif %}) {% for s in session_rows %} {% endfor %}
namestatetask-typerolemodeltask
{{ s.name }} {{ s.state }} {% for t in s.task_types %}{{ t }}{% endfor %} {% for r in s.roles %}{{ r }}{% endfor %} {% for m in s.models %}{{ m }}{% endfor %} {% for t in s.tasks %}{{ t }}{% endfor %}
{% endif %}

Dispatch substrate

Per-model dispatch counts from .context/dispatches.jsonl (CLI parity with fw orchestrator status, T-1788). Each row counts how many times the framework dispatch path actually routed work to that model — the "what got picked" view, complementing the learned-routing "what should win" view below. Synthetic rows (T-stress-*) are excluded from the headline counts.

{% if not substrate.available %}

substrate absent No dispatches.jsonl at {{ substrate.path }} yet. Run a dispatch (fw resolver dispatch or fw termlink dispatch) to seed it.

{% elif substrate.total == 0 and substrate.synthetic_total == 0 %}

no dispatches Substrate file exists but no real dispatches recorded yet.

{% else %}

{{ substrate.total }} real dispatch(es){% if substrate.synthetic_total %}, plus {{ substrate.synthetic_total }} synthetic (T-stress-*, excluded from the breakdown){% endif %}.

{% if substrate.by_model %}

By model

{% for row in substrate.by_model %} {% endfor %}
Model Dispatches Share
{{ row.model }} {{ row.count }} {{ "%.0f%%"|format(100 * row.count / substrate.total) }}
{% else %}

No model field on any dispatch row yet — substrate rows predate the model-capture path or model wasn't resolved.

{% endif %} {% if substrate.by_task_type %}

By task-type

{% for row in substrate.by_task_type %} {% endfor %}
Task-type Dispatches Share
{{ row.task_type }} {{ row.count }} {{ "%.0f%%"|format(100 * row.count / substrate.total) }}
{% endif %} {% if substrate.by_worker_kind %}

By worker-kind

{% for row in substrate.by_worker_kind %} {% endfor %}
Worker-kind Dispatches Share
{{ row.worker_kind }} {{ row.count }} {{ "%.0f%%"|format(100 * row.count / substrate.total) }}
{% endif %} {% endif %}

Outcome quality

Verification pass/fail per task_type from .context/dispatch-outcomes.jsonl (CLI parity with fw orchestrator status --outcomes, T-1749). Latest outcome per dispatch wins (T-1757 dedup rule). Verdict-style events (e.g. escalation-scan-v0.5) are counted in Outcomes but only contribute to passed/failed when they carry verification_passed.

{% if not outcome_quality.available or outcome_quality.total_outcomes == 0 %}

no outcomes No outcome events recorded yet (or dispatch-outcomes.jsonl is absent). Outcomes are written back when dispatch workers exit.

{% else %}

{{ outcome_quality.total_outcomes }} outcome event(s) across {{ outcome_quality.by_task_type|length }} task-type(s).

{% if outcome_quality.by_task_type %} {% for row in outcome_quality.by_task_type %} {% endfor %}
Task-type Passed Failed Total Pass rate
{{ row.task_type }} {% if row.passed %}{{ row.passed }}{% else %}0{% endif %} {% if row.failed %}{{ row.failed }}{% else %}0{% endif %} {{ row.total }} {% if row.passed + row.failed %}{{ "%.0f%%"|format(row.pass_rate * 100) }}{% else %}{% endif %}
{% endif %} {% endif %}

Workflow coverage

Workflow → dispatcher coverage matrix. Each workflow under .context/project/workflows/ declares an optional worker_kind field; the spawn driver (lib/spawn._DISPATCHERS) routes a subset of declarable values. Any workflow that declares a worker_kind without a registered handler is a runtime trap (T-1776 — surfaced as NotImplementedError). T-1798 ratchets this from a runtime trap to an audit-time visibility check; this panel renders the same data on the web.

{% if not workflow_coverage.available %}

unavailable Helper not importable.

{% else %} {% if not workflow_coverage.ok %}

FAIL {% if workflow_coverage.unroutable_workflows %} {{ workflow_coverage.unroutable_workflows | length }} of {{ workflow_coverage.workflows | length }} workflows declare an unroutable worker_kind. {% endif %} {% if workflow_coverage.pi_workflows_missing_provider %} {{ workflow_coverage.pi_workflows_missing_provider | length }} pi workflow(s) missing provider. {% endif %}

{% elif workflow_coverage.warn %}

WARN All {{ workflow_coverage.workflows | length }} workflows route, but {{ workflow_coverage.stale_workflows | length }} workflow(s) stale (no dispatch in 90d) — consider deprecating.

{% else %}

OK All {{ workflow_coverage.workflows | length }} workflows route to a registered dispatcher and pi workflows declare a provider.

{% endif %} {% for w in workflow_coverage.workflows %} {% set is_stale = workflow_coverage.stale_workflows and (w.name in (workflow_coverage.stale_workflows | map(attribute='name') | list)) %} {% endfor %}
Workflowworker_kindproviderRoutableLast dispatched
{{ w.name }} {% if w.worker_kind %}{{ w.worker_kind }}{% else %}— (interactive){% endif %} {% if w.provider %} {{ w.provider }} {% elif w.worker_kind == "pi" %} missing {% else %} {% endif %} {% if not w.worker_kind %} n/a {% elif w.worker_kind in workflow_coverage.routable %} yes {% else %} no {% endif %} {% if w.last_dispatched %} {{ w.last_dispatched[:10] }} {% if w.last_dispatch_task_id %} ({{ w.last_dispatch_task_id }}) {% endif %} {% if is_stale %}stale{% endif %} {% else %} never {% if is_stale %}stale{% endif %} {% endif %}

Routable dispatchers: {{ workflow_coverage.routable | join(', ') }}. Declarable but unroutable: {% if workflow_coverage.declarable_but_unroutable %} {{ workflow_coverage.declarable_but_unroutable | join(', ') }} (declared in lib/resolver.VALID_WORKER_KINDS but missing from lib/spawn._DISPATCHERS — declaring any of these in a workflow is a runtime trap). {% else %} none. {% endif %}

{% if workflow_coverage.pi_workflows_missing_provider %}

Missing provider: {{ workflow_coverage.pi_workflows_missing_provider | map(attribute='name') | join(', ') }} (pi workflows without a provider field — lib/spawn._spawn_pi raises SpawnError at runtime; T-1800 surfaces it at audit time).

{% endif %} {% endif %}

Learned routing

Per-task-type model preferences derived from route-cache.json ({{ learned.path }}). Each row shows the model with the highest historical success rate for that task type — the framework dispatch path (T-1669) consults this cache before falling back to FW_DISPATCH_MODEL_FOR_<TYPE> / FW_DISPATCH_MODEL_DEFAULT. Outcomes (success / failure) are recorded back into the same cache after each dispatch's worker exits, so the table grows as the agent dispatches more work.

{% if not learned.available %}

cache absent No route-cache.json at {{ learned.path }} yet. Spawn at least one fw termlink dispatch worker to seed it.

{% elif learned.total_stats == 0 %}

no recordings Cache file exists but no model_stats have been recorded yet. The headline mechanic ("orchestrator picks model based on task_type + historical success rates") needs at least one completed dispatch per task_type to start firing.

{% else %} {% for row in learned.by_task_type %} {% endfor %}
Task-type Best model Success rate Volume Last used All candidates
{{ row.task_type }} {{ row.best.model }} {{ "%.0f%%"|format(row.best.rate * 100) }} ({{ row.best.successes }}/{{ row.best.total }}) {{ row.best.total }} {{ row.best.last_used or "—" }} {% for c in row.candidates %} {{ c.model }} {{ "%.0f%%"|format(c.rate * 100) }} {% endfor %}

{{ learned.total_stats }} model/task-type stat(s) total. Resolution order in framework dispatch: explicit --model → route-cache best → env-per-type → env-default → none. Recording uses atomic write + flock for concurrency safety.

{% endif %}

Recent dispatches

Last {{ recent_dispatches|length }} fw termlink dispatch worker(s) from /tmp/tl-dispatch/. model_used and fallback_used are populated at dispatch time by the framework (T-1664) and by /opt/termlink's substrate CLI (scripts/tl-dispatch.sh, /opt/termlink T-1442 commit 143cd870). null means neither path resolved a value (no explicit --model and no FW_DISPATCH_MODEL_DEFAULT / DISPATCH_MODEL_FOR_<TYPE> set).

{% if recent_dispatches %} {% for d in recent_dispatches %} {% endfor %}
Name Task Task-type Model (asked) Model used Fallback? Status Started
{{ d.name }} {% if d.task %}{{ d.task }}{% endif %} {% if d.task_type %}{{ d.task_type }}{% endif %} {% if d.model %}{{ d.model }}{% else %}{% endif %} {% if d.model_used %}{{ d.model_used }}{% else %}null{% endif %} {% if d.fallback_used == True %}yes{% elif d.fallback_used == False %}no{% else %}null{% endif %} {{ d.status }} {{ d.started }}
{% else %}

No recent dispatches. Run fw termlink dispatch --task T-XXX --name foo --prompt "..." to populate.

{% endif %}

Reconsideration arc

Filed by T-1641 (multi-agent reconsideration of T-1061). Three arcs split the work so reviewers see policy / wiring / drift as distinct decisions, not one mega-arc.

{% for t in arc_tasks %}
{{ t.id }} — {{ t.name }}
type: {{ t.type }} · status: {% if t.completed %}work-completed {% elif t.status == 'started-work' %}{{ t.status }} {% else %}{{ t.status }}{% endif %} · horizon: {{ t.horizon }}
{% endfor %}

Background: T-1641 reconsideration artefact · /review/T-1641 for human decision · 10 worker reports under docs/reports/T-1641-worker-NN-*.md.