Install
$ agentstack add skill-uwilleer-preflight-preflight ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ● Shell / process execution Used
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
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
/preflightcommand itself) is the literal wordfast(case-insensitive), setcost_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:
{
"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_pathis set: Read that file, print contents verbatim, stop. - If
abortedis set: printaborted.reasonto user, stop. The plan needs iteration before a panel is worth running. - If
gateisnull: announce one line"no blockers — launching panel", go straight to Phase B. - Otherwise (
gateis an object): render the gate as an interactive picker viaAskUserQuestion(preferred) or fall back to text rendering. See "Gate rendering" below.
Gate rendering
Primary path — AskUserQuestion.
- Read
gate.gate_json_path(the structured form). The shape is{questions: [{id, header, type, prompt, options?, evidence_path}]}. - Partition questions by type:
binaryandchoice→ "structured", will go intoAskUserQuestion.open→ "free-form", handled separately as a text follow-up.
- If structured questions exist, build one
AskUserQuestioncall. Each entry:
question←q.promptheader←q.header(already ≤12 chars from Phase A)multiSelect: falseoptions[]← for eachoptinq.options[]:{label: opt.label, description: "+ \n− "}(newline-separated; both lines kept verbatim from Phase A).
- Issue the
AskUserQuestioncall. Phase A's pre-emit cap (> 4 questions → abort) guarantees ≤4 entries per call. - After the picker resolves, scan the picked option for each structured question — if
opt.pros(oropt.label) mentionspasteorprobe 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]). - For each free-form (
open) question: emitq.promptas 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_pathis 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(bypromptstring equality —question_textisq.promptverbatim). - If the answer string matches one of the option
labelvalues: record{id: q.id, answer: opt.key}(thea/b/ckey fromgate.json). - If the answer is the literal
"Other": readannotations[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 anOther+notes answer): write/aborted.jsonwith 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/fileslash-bearing token, afile:linereference, the literal wordactually, 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 withresume_from:andgate_answers: {questions: [{id, answer}, ...]}. Phase A will patchbrief.md/ground_truth.jsonand re-emit a (possibly empty) gate. Iterate untilgate == nullor user aborts. Apply the same heuristics to fallback-path replies. - Simple resolution (no abort signal, no material-change signal): write
/gate_answers.jsonas{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. Emitreport(or Readreport_pathand emit ifreport_too_long: true). Append warnings:- if
skipped_expertsnon-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. Readerror_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 withresponse.resume_token(step 4). Nodispatch[]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) withstarted_atset to the ISO-8601 UTC moment immediately before the call, andstatus: "ok"provisionally. Carryid,subagent_type,model,stepfrom the request;attempt: 1(orattempt: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_toverbatim. Update thedispatch[]entry withcompleted_atandduration_ms. - On failure (timeout, malformed JSON the agent itself rejected, exception):
- If
request.on_failure == "mark_skipped": update the dispatch[] entry withstatus: "skipped",reason: "",completed_at,duration_ms, and continue. - If
request.on_failure == "abort": update the dispatch[] entry withstatus: "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
- Source: uwilleer/preflight
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.