# Preflight

> >

- **Type:** Skill
- **Install:** `agentstack add skill-uwilleer-preflight-preflight`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [uwilleer](https://agentstack.voostack.com/s/uwilleer)
- **Installs:** 0
- **Category:** [AI & ML](https://agentstack.voostack.com/c/ai-and-ml)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [uwilleer](https://github.com/uwilleer)
- **Source:** https://github.com/uwilleer/preflight/tree/main/skills/preflight

## Install

```sh
agentstack add skill-uwilleer-preflight-preflight
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Preflight — orchestration shell

You are the **orchestrator** of a pre-write review. The user gives you an artifact (plan file, design spec, RFC, or a proposal made earlier in the conversation). You do NOT run the 12-step pipeline inline. You spawn sub-coordinator subagents for the three phases — Phase A (steps 0–6), Phase B (steps 7–9), Phase C (steps 10–11) — and relay structured handoffs between them, the user, and the `Agent` calls each phase needs.

Under v0.7.0 (post-#19), Phase B and Phase C use a **dispatch loop**: the coordinator returns one of `complete | dispatch | error` per spawn. On `dispatch`, you execute the requested `Agent` calls, write their results to the workspace, and re-spawn the coordinator with `resume_token`. This is necessary because empirically `Agent` is not delivered to spawned subagents in current CC builds — only the main session can issue `Agent` calls. The coordinator is a state machine over workspace files; you are the executor. Per Phase B run: up to 5 coordinator spawns (initial + 4 round-trips). Per Phase C run: up to 3 (initial + rubber-duck + compactor). Phase A still runs as a single spawn (its only `Agent`-needing step, the selector, has an inline-fallback).

This split exists for one reason: running the full pipeline inline would burn 80–150k of main-session context per invocation (workspace files, expert reports, synthesizer JSON, render scratch). Sub-coordinator dispatch keeps your context at ~50k under v0.7.0 (was ~25k under v0.6.x — the round-trip pattern adds some overhead, but is still well under the inline cost). The coordinators do the thinking; you do the `Agent` execution and route handoffs.

## Cost profile detection

Before spawning Phase A, extract `cost_profile` from the `/preflight` invocation:

- If the first non-whitespace token of the argument (after stripping the `/preflight` command itself) is the literal word `fast` (case-insensitive), set `cost_profile = "fast"` and treat the remainder of the argument as the actual artifact path/text.
- Otherwise `cost_profile = "thorough"`.

Examples:
- `/preflight fast path/to/spec.md` → `cost_profile = "fast"`, `user_request = "path/to/spec.md"`
- `/preflight path/to/spec.md` → `cost_profile = "thorough"`, `user_request = "path/to/spec.md"`
- `/preflight fast` (no artifact) → `cost_profile = "fast"`, `user_request = ""` (Phase A will error on empty request — surface to user)

## Output language

Before spawning Phase A, determine the user's working language from this session: the system prompt's language directive (e.g. "Always respond in Русский"), recent user turns, and the natural-language sections of the artifact or `/preflight` argument. Encode it as a short free-form string (`"Russian"`, `"English"`, `"German"`, …). Default to `"English"` if the signal is absent or mixed. Pass this string as `user_language` in the JSON input to every phase.

The boundary, enforced inside sub-coordinators: machine artefacts (`brief.md`, role-KB, JSON, expert prompts) stay in English — lower tokens, more reliable expert behaviour. User-facing prose (`gate.md`, decision cards in `synth_result`, `report.md`, polished report) is rendered in `user_language`. Technical tokens — code, `file:line` refs, command syntax, JSON keys, role names, CLI flags — stay verbatim regardless of language.

## Model selection policy

Sonnet floor for every task that requires judgment — coordinators, selector, every expert role, synthesizer, verifier, adversarial round, rubber-duck. Haiku is reserved for **mechanical text transforms** (currently only the KB compactor in Phase C step 11 — fixed-schema dedup + reformat with no severity calls). Opus for adversarial-reasoning roles (security, contrarian-strategist) and for the synthesizer when the panel is large or in conflict. Per-task choice lives in `request.model_hint` (set by the coordinator at dispatch construction); main passes it as `Agent`'s `model` parameter.

## Timing instrumentation

Each coordinator spawn and each main-executed `Agent` dispatch is recorded in `$WORKSPACE/_index.json` so latency is measurable without re-running the pipeline. Both `_index.json.coordinator_spawns[]` and `_index.json.dispatch[]` are **optional and additive** — older runs lacked them; readers must treat absence as `[]`. No handoff-schema change.

**Sub-coordinators write `coordinator_spawns[]`.** Each Phase A/B/C subagent appends an entry on entry and closes it on emit (start/end timestamps + which `action` it returned). See the "Timing instrumentation" section in each `meta-agents/sub-coordinator-phase-{a,b,c}.md` for the per-phase contract.

**Main writes `dispatch[]`.** For every `Agent` call you execute as part of a dispatch handoff, record one entry in `_index.json.dispatch[]`. Append before the call, mutate on completion. Shape:

```json
{
  "id": "",
  "subagent_type": "",
  "model": "",
  "step": "",
  "started_at": "",
  "completed_at": "",
  "duration_ms": ,
  "status": "ok" | "skipped" | "error",
  "reason": "",
  "attempt": 
}
```

For parallel dispatches (single message with N `Agent` calls), record `started_at` for each request just before issuing the batch, then `completed_at` per request as each result lands. For sequential dispatches, record per-call as you go. Retries (transient failures with the cap-at-2 retry per the coordinator's contract) append a new entry with `attempt: 2` rather than overwriting `attempt: 1` — the per-attempt history is the load-bearing telemetry.

The expanded `dispatch[]` shape is **a superset** of the prior `{role, model, step}` shape that step-7 instructions already wrote — old runs and old code paths that consume the lighter shape still work; new fields are additive.

**Why optional:** runs older than this instrumentation must remain readable. Future tooling (e.g. `evals/timing_report.py`) parses both arrays if present, computes per-phase latency = `max(emit_at) - min(started_at)` and per-step dispatch latency = `max(completed_at where step == X) - min(started_at where step == X)`. Without this telemetry, optimization ideas are guesses.

## Three-phase protocol

### Phase A — init, brief, gate

Spawn:

```
Agent(
  subagent_type: general-purpose,
  description: "Preflight phase A — init+gate",
  prompt: 
         + "\n\n## Invocation inputs\n\n"
         + JSON.stringify({
             cwd: ,
             user_request: ,
             now_iso: ,
             user_language: ,
             cost_profile: ,
             resume_from: null,
             gate_answers: null
           })
         + "\n\nReturn ONLY the JSON handoff specified in the output section. No prose."
)
```

Choose model per-task: small for short artifacts and code-touching plans where step 4 dominates; upgrade for long architecture-only artifacts where the brief itself is judgement-heavy.

Parse the return against `schemas/phase-handoff.json#/definitions/phase_a_output`. On parse failure, retry once with a terser prompt; second failure → stop and surface to user.

**Handle the handoff:**

- If `error_path` is set: Read that file, print contents verbatim, stop.
- If `aborted` is set: print `aborted.reason` to user, stop. The plan needs iteration before a panel is worth running.
- If `gate` is `null`: announce one line `"no blockers — launching panel"`, go straight to Phase B.
- Otherwise (`gate` is an object): render the gate as an interactive picker via `AskUserQuestion` (preferred) or fall back to text rendering. See "Gate rendering" below.

### Gate rendering

**Primary path — `AskUserQuestion`.**

1. Read `gate.gate_json_path` (the structured form). The shape is `{questions: [{id, header, type, prompt, options?, evidence_path}]}`.
2. Partition questions by type:
   - `binary` and `choice` → "structured", will go into `AskUserQuestion`.
   - `open` → "free-form", handled separately as a text follow-up.
3. If structured questions exist, build **one** `AskUserQuestion` call. Each entry:
   - `question` ← `q.prompt`
   - `header` ← `q.header` (already ≤12 chars from Phase A)
   - `multiSelect: false`
   - `options[]` ← for each `opt` in `q.options[]`: `{label: opt.label, description: "+ \n− "}` (newline-separated; both lines kept verbatim from Phase A).
4. Issue the `AskUserQuestion` call. Phase A's pre-emit cap (`> 4 questions → abort`) guarantees ≤4 entries per call.
5. After the picker resolves, scan the picked option for each structured question — if `opt.pros` (or `opt.label`) mentions `paste` or `probe output`, emit a one-line follow-up like `"paste the probe output now (or 'skip'):"` and append the user's verbatim reply to that question's answer as `\n`. The deploy-state gate question is the canonical case (option `[a]`).
6. For each free-form (`open`) question: emit `q.prompt` as a single short text line and wait for the user's reply. Collect each reply alongside the picker answers.

**Fallback path — text render.** Use the legacy text path when *any* of these holds:
- `gate.json_path` is missing, unreadable, or schema-invalid.
- Every question has `type: open` (nothing structured to render).
- `render_too_long: true` (gate.md too large to inline).

In the fallback path, emit `gate.render` verbatim — or, if `render_too_long: true`, Read `/gate.md` and emit that. Wait for the user's free-form reply (e.g. `"1=a 2=b 3=actually X"`).

### Gate iteration (between A and B)

Translate the user's answers to the on-disk format and decide the next move.

**Per-answer mapping (primary path).**

For each picker answer (`AskUserQuestion`'s `answers[question_text]`):
- Look up the matching question in `gate.json` (by `prompt` string equality — `question_text` is `q.prompt` verbatim).
- If the answer string matches one of the option `label` values: record `{id: q.id, answer: opt.key}` (the `a`/`b`/`c` key from `gate.json`).
- If the answer is the literal `"Other"`: read `annotations[question_text].notes` (free-form text the user typed). Record `{id: q.id, answer: ""}`. Treat `"Other"` *without* notes as the empty string `""`.

For each free-form follow-up reply (open question): record `{id: q.id, answer: ""}`.

**Per-answer mapping (fallback path).** Parse the user's free-form reply as before — `"1=a 2=b 3=text"` style or natural-language picks among the offered options.

**Decide the next move based on the assembled answers:**

- **Abort** (user says `"stop"`, `"no"`, `"cancel"` anywhere in the gate, including via an `Other`+notes answer): write `/aborted.json` with the user's reason, stop.
- **Material change to load-bearing facts.** Trigger when *any* answer contains free-form content with at least one of these signals: a `path/to/file` slash-bearing token, a `file:line` reference, the literal word `actually`, a four-or-more-digit line number adjacent to a file token, or explicit contradiction of a brief fact (e.g. `"the migration target is X, not Y"`). Re-spawn Phase A with `resume_from: ` and `gate_answers: {questions: [{id, answer}, ...]}`. Phase A will patch `brief.md` / `ground_truth.json` and re-emit a (possibly empty) gate. Iterate until `gate == null` or user aborts. Apply the same heuristics to fallback-path replies.
- **Simple resolution** (no abort signal, no material-change signal): write `/gate_answers.json` as `{questions: [{id, answer}]}` directly, proceed to Phase B.

If the answer set is ambiguous (e.g. `Other`+notes that could be either a material change or just a reworded pick), ask one short clarifying question — do not guess.

### Phase B — dispatch, synth, render (round-trip loop, v0.7.0)

Phase B coordinator under v0.7.0 returns one of `complete | dispatch | continue | error` per spawn. Main session runs a loop: spawn coordinator → if `dispatch`, execute the requested `Agent` calls and write results to `save_to` paths → re-spawn coordinator with `resume_token` set; if `continue`, re-spawn immediately with `response.resume_token` (no Agent calls). Loop terminates on `complete` or `error`. Worst case: 6 coordinator spawns (initial + step-7 dispatch + step-7-validate + step-7.5 dispatch + step-8 dispatch + step-8.5 dispatch). Common case: 4–5. See `docs/specs/2026-05-03-phase-b-main-driven-dispatch.md`.

The loop matters because empirically `Agent` is not delivered to subagent contexts in current CC builds — only the main session can issue `Agent` calls. The coordinator is now a state machine over workspace files; main is the executor.

**1. Initial spawn:**

```
Agent(
  subagent_type: general-purpose,
  description: "Preflight phase B coordinator",
  prompt: 
         + "\n\n## Invocation inputs\n\n"
         + JSON.stringify({
             workspace_path: ,
             gate_answers_path: /gate_answers.json"> | null,
             user_language: ,
             cost_profile: ,
             resume_token: null
           })
         + "\n\nReturn ONLY the JSON handoff specified in the output section. No prose."
)
```

Coordinator model: **sonnet floor** — even though the coordinator is "just" state inspection + dispatch construction, picking the right `model_hint` per request and the right `subagent_type` per step is a judgment call that haiku gets wrong often enough to matter (verified empirically — the dispatch payloads it builds are the load-bearing input to every expert that follows). Upgrade to opus only if experiments show coordinator quality issues. Expert / synthesizer / verifier model choice is per-request via `request.model_hint`, set by the coordinator at dispatch construction time — pass it as Agent's `model` parameter when executing the dispatch.

**2. Loop on `response.action`:**

Parse the return against `schemas/phase-handoff.json#/definitions/phase_b_output`. On parse failure, retry the spawn once with a terser prompt; second parse failure → stop and surface to user.

- **`"complete"`** — terminal success. Emit `report` (or Read `report_path` and emit if `report_too_long: true`). Append warnings:
  - if `skipped_experts` non-empty: `"⚠ skipped experts:  (reports failed twice)"`
  - if `drift_refreshed: true`: `"ground_truth refreshed at synth time — repo HEAD moved during review"`

  Spawn Phase C. **Done.**

- **`"error"`** — terminal failure. Read `error_path`, print contents verbatim, stop. Do NOT spawn Phase C.

- **`"continue"`** — coordinator completed a write-only step and needs a fresh context window. No Agent calls. Re-spawn coordinator immediately with `response.resume_token` (step 4). No `dispatch[]` entries written for this round-trip.

- **`"dispatch"`** — execute the dispatch (step 3 below), then re-spawn coordinator (step 4).

**3. Executing a dispatch:**

Read `response.dispatch.requests[]`. If `parallelism == "parallel"`, send a **single message** containing N `Agent` tool calls — one per request (parallel execution). If `parallelism == "sequential"`, send them one at a time.

For each request:
- **Append a `dispatch[]` entry** (see "Timing instrumentation" above) with `started_at` set to the ISO-8601 UTC moment immediately before the call, and `status: "ok"` provisionally. Carry `id`, `subagent_type`, `model`, `step` from the request; `attempt: 1` (or `attempt: ` on a retry — append, do not overwrite).
- Spawn `Agent(subagent_type=request.subagent_type, model=request.model_hint, description=request.description, prompt=request.prompt)`.
- On success: Write the response body to `request.save_to` verbatim. **Update the `dispatch[]` entry** with `completed_at` and `duration_ms`.
- On failure (timeout, malformed JSON the agent itself rejected, exception):
  - If `request.on_failure == "mark_skipped"`: **update the dispatch[] entry** with `status: "skipped"`, `reason: ""`, `completed_at`, `duration_ms`, and continue.
  - If `request.on_failure == "abort"`: **update the dispatch[] entry** with `status: "error"`, `reason`, `completed_at`, `duration_ms`. Then stop the loop, surface error to the user, do NOT spawn Phase C.

Do NOT modify `request.prompt` — coordinator built it deliberately. Main is a dumb executor.

**4. Re-spawn coordin

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [uwilleer](https://github.com/uwilleer)
- **Source:** [uwilleer/preflight](https://github.com/uwilleer/preflight)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** yes
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-uwilleer-preflight-preflight
- Seller: https://agentstack.voostack.com/s/uwilleer
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
