Enforcement check

v7 unifies enforcement into one report. The CLI haft check is the CI-friendly entry point; haft_query(action="check") is the plugin-mode parity for embedded host agents. Both consume the same Go helpers and produce JSON with identical fields — a contract test asserts the shape.

What the report covers

  • Stale — decisions and artifacts whose valid_until is past, or whose evidence has decayed (R_eff < 0.5; AT-RISK at < 0.3).
  • Drifted — decisions with file drift on baselined paths.
  • Unassessed — active decisions with no measurement yet.
  • Coverage gaps — claims with no satisfying evidence.
  • Spec health — drift on approved SpecSections (spec_section_drifted), missing baselines on active sections (spec_section_needs_baseline), time-based staleness (spec_section_stale for active sections whose valid_until is past today), and L0/L1/L1.5 structural carrier findings from haft spec check.

CLI: haft check

cd path/to/your/project
haft check          # human-readable summary
haft check --json   # structured JSON for CI

Exit code:

  • 0 — clean. No stale, no drift, no coverage gaps, no spec issues.
  • 1 — at least one finding. CI gates can branch on this.

Sample human output on a project with a stale section:

haft check: governance debt found (2 finding(s))
stale: 0
drifted: 0
unassessed: 0
coverage gaps: 0
spec health: 2

Spec Health
- [error/spec_section_needs_baseline] TS.role.001 — section "TS.role.001" is active
  but has no baseline; the operator has not yet approved it through the onboarding
  method
  next_action: haft_spec_section(action="approve", section_id="TS.role.001") to
  record a baseline
- [error/spec_section_stale] TS.role.001 — section "TS.role.001" is active but
  valid_until 2025-01-01 expired 482 day(s) ago
  next_action: triage staleness on "TS.role.001": rebaseline if the claim is still
  current (extend valid_until in the carrier and run haft_spec_section
  action=rebaseline), reopen if the claim needs review, or deprecate the section

MCP: haft_query(action="check")

Embedded host agents in plugin mode call the same machinery through MCP:

haft_query(action="check")

Returns a JSON object with the same field layout as haft check --json. The host agent reads spec_health, sees a typed spec_section_drifted finding, and proposes the next action directly:

haft_spec_section(action="rebaseline",
                  section_id="TS.role.001",
                  reason="role narrowed after stakeholder review")

Without typed access the host agent would be parsing CLI prose. With it, the triage loop is fully structured.

When to use which

  • CI pipelineshaft check in a pre-merge check. Exit-code gates the merge.
  • Pre-commit hook — same command. Optionally narrow to haft spec check for spec-only feedback.
  • /h-verify in plugin mode — automatically calls haft_query(action="check") as discovery, then walks the operator through triage.

Distinguishing status from check

haft_query(action="status") is the at-a-glance overview — counts, recent activity, readiness state. haft_query(action="check") is the CI-actionable enforcement view — every individual finding with code, level, and next-action hint. Use status when reporting; use check when acting.

Schema parity guarantee

The contract is enforced by tests:

  • TestHandleQuintQuery_CheckMatchesCLIJSON decodes both MCP and CLI --json into the same Go struct and asserts field-by-field equality.
  • TestHandleToolsList_NoToolDeclaresTopLevelCompositors iterates every advertised tool and rejects top-level allOf/oneOf/ anyOf in input schemas (a regression that previously took the whole haft MCP server offline at the host LLM API boundary).