---
name: missing-contract-action-detect
description: Engine substrate for the heartbeat-driven detector that finds registered tools whose contract_action is not mapped to any trust domain in domains.yaml, and proposes the mapping for user approval.
title: Missing Contract Action Detector
category: _meta
status: active
author: agentic
metadata:
  priority: 4
---
## Purpose

Closes the third Rule 21 gap named in CLAUDE.md but unaddressed before
this skill: a tool is registered with `contract_action="my_action"` but
that action string isn't listed under any domain in
`domains.yaml`. The action falls through to the catch-all `general`
domain, which means trust-evolution can't gate it independently AND
the per-domain Bayesian posterior can never apply. This is the exact
"tools without a domain mapping is a code smell" failure mode CLAUDE.md
Rule 16 explicitly names.

This detector closes that gap: it walks the live `ToolRegistry`,
collects every `contract_action` value, subtracts the union of
`contract_actions:` lists across `domains.yaml`'s domains and the
`routine_actions:` list, and proposes the missing actions for user
approval. Approval mutates `domains.yaml` to attach the action to the
proposed domain (mirror of `taxonomy_proposals.approve_proposal`'s
override-merge pattern).

**Why this is a Rule 21 capability and not just a CI lint**: a CI
failure is a developer notification that decays at HEAD. A
`/proposals` inbox entry stays surfaced until the user takes action,
and the approval merges the fix into the user's vault config —
visible, auditable, reversible.

## When this fires

Heartbeat-driven, daily by default. Pure deterministic scan — no LLM,
no log analysis, just set arithmetic over registry tools + domains.yaml.

For each unmapped contract action, the proposal carries:

* the **list of registered tools** that use this action (so the user
  sees the blast radius)
* a **suggested domain** picked by lexical similarity — actions
  starting with `read_*` go to a read-domain hint, `send_*` to outreach,
  `write_*` to knowledge management, etc. The hint is **only a hint** —
  the user picks the real target via the approval flow.
* an **idempotent stable id** keyed on the action string

## What it doesn't do

* **Never proposes for actions already mapped.** Membership in any
  domain's `contract_actions:` short-circuits.
* **Never proposes for `routine_actions`.** Those are deliberately
  un-domained (they bypass trust-gating entirely).
* **Never auto-mutates `domains.yaml` outside an approval flow.** The
  detector ONLY writes to the pending queue; the approval flow is what
  reaches into the override file.
* **Never proposes for tools without a `contract_action`.** Tools that
  declared `contract_action=None` are a different gap (a missing
  contract action assignment ON the tool). That's a Rule 16 violation
  the lint test already catches; this detector skips them.

## Output format

For each unmapped action, the detector writes one proposal to:

```
<vault>/agntspace/memory/pending/contract-action-proposals/cap-<sha8>.json
```

The proposal carries:

* `kind: "contract-action"`
* `target`: the action string (e.g. `my_new_action`)
* `title`: "Map contract action: <name> → <suggested-domain>"
* `description`: short one-paragraph rationale
* `rationale`: longer explanation listing the tools using this action
  + which trust-half-life the suggested domain has
* `examples`: tool names using this action
* `payload`:
  - `using_tools`: list of tool names
  - `suggested_domain`: domain name from `domains.yaml`
  - `existing_domains`: full list of domains (for the user's pick)

## Tuning knobs (`config/detect.yaml`)

* `enabled` (bool, default true) — gate the whole detector
* `min_using_tools` (int, default 1) — minimum tools using the action
  before a proposal is emitted
* `skip_if_pending` (bool, default true) — dedup vs in-flight
  proposals
* `cadence_hours` (int, default 24) — heartbeat interval
* `domain_suggestion_hints` (map) — prefix-to-domain hints used to
  pick the suggested domain (`read_` -> "Knowledge management",
  `send_` -> "Contact outreach", etc.)
* `default_suggested_domain` (string, default "Knowledge management")
  — fallback when no prefix hint matches

Edit YAML, restart server, no code change. (Rule 12.)

## Approval semantics

Approval **mutates** the vault override at
`<vault>/agntspace/system/skills/core/trust-evolution/config/domains.yaml`
to add the action under the proposed domain. The mutation is
idempotent — re-applying the same approval does nothing. If the vault
override doesn't exist yet, the merge writes the file with the union
of (shipped defaults + the new mapping).

Rejection is silent — removes the pending file, emits a
`user_override` activity event. The action stays unmapped (still
falling through to `general` domain).