---
name: preflight-structural-gap
description: Engine substrate for chat-turn pre-flight that surfaces structurally-incomplete skill matches to the agent BEFORE LLM generation, so the agent can tell the user explicitly what is missing instead of producing a generic fallback.
title: Pre-flight Structural Gap Detector
category: _meta
status: active
author: agentic
metadata:
  priority: 4
---
## Purpose

Closes the load-bearing gap from the
`feedback_main_agent_orchestrator.md` rule (2026-04-28): the main
agent must orchestrate everything and never silent-fail. Before this
substrate, the chat path knew about every active skill (full bodies
in the system prompt) but had no per-turn signal saying "the user's
input matches a skill that is structurally incomplete — don't produce
a generic fallback that pretends to satisfy it."

The canonical regression: user typed `archer futura presentation
style`, the active `archer-presentation` skill matched semantically
and lexically, but it had no `triggers:`, no `tools_granted:`, and
referenced no registered theme. The agent built a generic deck. With
this substrate, the pre-flight detects the match + the gap, injects a
system-message hint into the turn telling the agent to **proceed
with best-effort using the skill body's content as a blueprint** AND
**disclose the gap transparently in the reply**. The agent renders
the deck the user asked for using the brand specs in the skill body,
THEN tells the user "for one-click automation next time, add these
fields to the skill". Never refuse, never ask permission, never
silent fallback.

## When this fires

At the top of every chat turn (`Agent.chat_stream`), AFTER the user
message is added to history but BEFORE the LLM is called.

The pre-flight runs:

1. **Lexical match** via `SkillLoader.match_skills(user_input)` —
   trigger-based, total, sub-millisecond.
2. **Semantic match** via `SkillLoader.find_matching_skills(...)` —
   Voyager-style cosine match against per-skill embeddings. Fails
   open if the embedder isn't wired (no Ollama, no `bge-m3`).
3. **Structural classification** of every match: `executable` vs
   `structurally_incomplete` vs `broken_grants`.
4. **Hint injection**: when the top match has gaps, the rendered
   hint is prepended to the per-turn message list as a system-role
   message.

Hints are scoped to the turn — they don't modify the base system
prompt (preserves prompt cache) and they don't get persisted to
conversation history.

## What it doesn't do

* **Never auto-fixes the skill.** Editing SKILL.md frontmatter is a
  separate flow (the skill-completeness detector queues a proposal in
  `/proposals` for the user to act on).
* **Never invents triggers / tools_granted values.** Domain-specific.
* **Never blocks the LLM call.** A failed pre-flight (broken
  embedder, malformed YAML) silently returns an empty hint list and
  the chat proceeds normally.
* **Never leaks across turns.** The hint applies only to the turn
  whose user input triggered the match.

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

* `enabled` (bool, default true) — gate the whole pre-flight
* `lexical_max_matches` (int, default 3) — cap on trigger-match
  results per turn
* `semantic_top_k` (int, default 5) — top-K semantic candidates
* `semantic_min_score` (float, default 0.55) — cosine floor below
  which semantic matches are dropped
* `min_user_input_chars` (int, default 10) — short inputs (e.g.
  "ok", "thanks") bypass pre-flight to keep pure-chat noise quiet
* `max_hints_per_turn` (int, default 1) — cap on injected hints
* `hint_template` — Python `.format()` template with placeholders
  `{skill_name}`, `{skill_title}`, `{gap_summary}`, `{match_kind}`,
  `{score}`. Tune the agent's phrasing without touching code.

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

## Why this isn't redundant with the skill-completeness detector

* **skill-completeness-detector** (heartbeat-driven, daily): finds
  every structurally-incomplete skill in the vault, queues
  proposals for user review at `/proposals`. The user fixes them
  on their own schedule.
* **preflight-structural-gap** (chat-turn-driven, real-time): when
  the user's CURRENT input triggers a match against an incomplete
  skill, surfaces the gap to the agent IN THIS TURN so the agent's
  reply is honest about the gap instead of producing a generic
  fallback.

The two close the loop in different time windows: one says "fix
these", the other says "you can't pretend to do this right now".

## Hint shape

The default template renders something like:

```
[Pre-flight skill match] The user's input matches the skill
`archer-presentation`. Structural gaps detected: no triggers; no
tools_granted. PROCEED with the request using the skill body's
content as your best-effort blueprint — gaps mean the skill lacks
one-click automation wiring, NOT that you should refuse. Pick
reasonable defaults for unspecified detail; ask at most ONE sharp
clarifying question, only if a true blocker. In your reply, briefly
note what would need adding to the skill for full automation next
time. Best-effort execution + transparent gap disclosure — never
silent fallback, never false refusal.
```

The agent reads this in the turn's message list, executes the
request following the skill body's instructions, and in its reply
discloses the gaps + suggests fixes. The user gets the artifact they
asked for AND a concrete path to make next runs fully automated.
