| name | flight-trajectory |
| description | Drive a long-running build along one path of an explore-latent DAG, stopping to ask you only the decisions that matter. The skill is the CONDUCTOR: it loads the possibility-graph (whole tree), has you pick a path at gate-0, then fires a headless leg-engine workflow that builds each milestone bounded by its minimum-viable-test — flying all independent legs in parallel waves, parking expensive gates for human batch-resolution, and returning only when nothing can proceed without human input. Use after explore-latent has emitted a validated dag.json. Trigger on 'flight-trajectory', 'fly the path', 'run the build', 'walk the trajectory', 'long-running build', 'drive this build', 'execute the path', 'arm the build job'. Do not trigger for charting the map (use explore-latent), for shaping (use intent-shape), or for a single known edit (just do it). |
flight-trajectory
The third tool in the stack: intent-shape (the walls) → explore-latent (the map) → flight-trajectory (flying one route). intent-shape bounds what the thing is; explore-latent charts how you get there and where it forks; this skill walks one charted path as a long-running, overnight-capable build that stops to ask you only the decisions that matter.
Not a planner. Not a path-ranker. A driver. The build moves on its own between forks; at each fork it either decides for itself (cheap, reversible) or parks and notifies you (expensive, irreversible), while continuing to fly all independent branches.
The split — conductor vs engine
Two layers, and keeping them apart is the whole design:
- This skill is the CONDUCTOR — it runs in the main loop (the only layer where a human gate is legal). It arms the flight, fires the engine, catches completions, runs harvest. You are in the loop here.
flight-trajectory.js is the ENGINE — a headless Workflow that walks the DAG in wave-parallel waves: flies all independent legs simultaneously, auto-decides cheap gates in-run, parks expensive gates, integrates mutating legs serially to prevent conflict, and writes durable state after every wave. It returns only when nothing can fly without a human.
A workflow cannot ask a human mid-run. So the human gate is a seam between runs: the engine parks, notifies you, writes parked.md, and the conductor re-arms the next segment with your batch-resolved gates in resolvedGates[]. Continuity lives in state.json — each segment cold-starts from it.
Input
A validated dag.json from explore-latent (its prose+dag emit mode). The schema is the authority: see dag-contract.md. The graph carries every charted path plus pruned branches for reroute. If explore-latent emitted prose only, it is not ready for flight — send it back for DAG mode, or hand-author the DAG against the contract.
The routing key: each gate's reversibility: cheap | expensive. Cheap → engine auto-decides with logged triad. Expensive → park + notify + fly other branches.
Flight workspace layout
Every flight lives in:
.ai/flight/<pathId>/
dag.json # the committed graph; written at arm time
state.json # live node-status map; scribe overwrites after every wave
parked.md # human-readable parked gate frames + cold questions; written when engine returns NEEDS_INPUT
flight-log.md # append-only wave log; scribe appends after every wave
artifacts/ # proto-buddy .html tabs written by engine mid-flight; conductor starts server at harvest
The conductor loop
0. Present the possibility menu
Before arming anything, present every path from the dag.json as a possibility menu — this is where the architect's judgement enters for the first time. Each entry shows:
archetype and thesis (from the path object)
- First milestone description and its hypothesis (the riskiest assumption)
- Which inviolate it leans on hardest
- What it explicitly trades away
Present all entries. Do not preselect, do not recommend, do not trim. The architect reads the menu and chooses — there may be multiple picks (fly two paths in sequence, or pick one and note the others for later). This is the moment where intangible judgement shapes the work; do not shortcut it.
1. Arm
Validate first. Read the dag.json. Confirm every path's edges resolve, no cycles, no dangling leads_to ids, no gate options pointing to nonexistent nodes. Stop and report before firing anything on a broken graph.
Gate-0 — path selection. Present every path with its archetype and thesis. Use AskUserQuestion. One question: which pathId? This is the highest-stakes decision; do not bundle other questions into it. If the architect picked multiple paths in step 0, confirm the order here — flight-trajectory flies one path at a time.
Batched arming question. Immediately after gate-0 resolves, send ONE AskUserQuestion covering all of:
notifyPolicy: vsay | push | both | none
dryRun first? (strongly recommended before any overnight flight — see Refusals)
- Second-tranche consent: only relevant if you arm this flight under a turn token budget (a
+<N>k directive on the arming turn). If one is set and the engine returns PAUSED, auto-refire once only? (yes/no)
PAUSED fires only when a turn token budget is active — the engine's reserve guard reads the Workflow runtime's budget.total, which is null unless you armed under a +<N>k directive. With no turn budget the flight runs to COMPLETE or NEEDS_INPUT and never pauses, and the second-tranche consent is moot. There is no separate ceiling arg to pass — the turn budget is the ceiling.
Do not fire the engine until both gate-0 and the arming question are answered.
Create the workspace. Write .ai/flight/<pathId>/dag.json (the committed graph). Do NOT pre-seed state.json — pass state: null on first arm. The engine bootstraps the full state shape (including checkpoints, parked, cold, log, wave) internally when savedState is null. Hand-seeding only the nodes map omits required fields and crashes the first wave. On re-arm, pass the live state.json contents read from disk.
Fire the engine. Call:
Workflow({
scriptPath: '<skill-dir>/flight-trajectory.js',
args: {
graph,
pathId,
dryRun, // bool; omit (or false) for a real flight
state: null, // null on first arm — engine bootstraps; pass state.json contents on re-arm
resolvedGates, // [] on first arm; populated at every re-arm
flightDir, // absolute path to .ai/flight/<pathId>/
notifyPolicy, // 'vsay' | 'push' | 'both' | 'none'
legModel, // optional; inherits session model if omitted
legEffort, // optional
startedAt, // pass Date.now() from the conductor — engine cannot call it directly
}
})
The Workflow runs in the background. A completion notification re-invokes this session with the result.
2. Heartbeat
Immediately after firing: set a ScheduleWakeup for 3600 seconds as a fallback monitor.
On wakeup — check state.json:
- If it does not exist yet: the engine is still in wave 1 (the scribe writes
state.json only after a wave completes). Reset the wakeup for another 3600s; do not declare a stall.
- If mtime advanced since arm: engine is still flying; reset the wakeup for another 3600s.
- If mtime is stale (no writes in >3600s): engine is likely stalled. Notify the architect per
notifyPolicy, surface the last wave log, and offer to resume from state.json (re-arm with state populated, empty resolvedGates, same flightDir).
3. On engine completion
Branch on status:
NEEDS_INPUT — nothing can fly without a human; ≥1 expensive gate is parked.
Write parked.md to the flight workspace. Format:
# Parked gates — <pathId>
## <node-id>: <description>
**What you'll know:** <what_youll_know>
**Decision criteria:** <decision_criteria>
**Inviolate touched:** <inviolate_touched>
Options:
- `<label>` — <summary> (leads to: <leads_to>)
- `<label>` — <summary> (leads to: <leads_to>)
Artifact: <kind> — <brief>
---
Append any cold questions from resumeState.cold at the bottom of parked.md in unified cold-lane format. Engine cold items carry {node, question} — node is the origin leg, not a fold-target; the architect assigns the fold-target at harvest:
- [ ] <question> *(from: flight, origin: <node>)* → folds into: [architect assigns]
Notify the architect per notifyPolicy. Stop. Do not auto-resolve any gate. Wait for harvest (step 4).
COMPLETE — path landed.
Append a harvest record to .ai/flight/<pathId>/flight-log.md:
- Each leg checkpoint:
node, hypothesis, what_proved, mvt_overrun. Gate checkpoints carry a different shape — node, decision, predicted, observed, gap — render both.
stats block from the return: total, done, pruned, waves, overruns, maxWaveWidth, coldQuestions
- Any remaining cold questions
Notify per notifyPolicy. Surface any stats.overruns (legs that breached their mvt ceiling) — these are the depth-trap warnings.
BLOCKED — a leg failed its done_condition, or an integration conflict occurred.
Notify with the blocking node id and the last log message for it. Decide with the architect: retry (re-arm from state.json), reroute into a pruned[] sibling, or abandon. Do not silently retry.
PAUSED — budget reserve tripped. (Only reachable when the flight was armed under a turn token budget — see Arm.)
Notify with remaining budget and wave count. If the architect pre-authorized second-tranche consent at arming: re-arm once only with the same state and empty resolvedGates. Do not auto-refire a second time.
ABANDONED — an expensive gate chose an ABANDON option (or the architect killed the path).
Report what was learned from checkpoints. Offer to reroute into a pruned[] sibling or return to explore-latent.
ERROR — safety counter (500 waves) tripped, or a bad resolvedGates pick.
Surface the message and full log. Do not retry automatically; diagnose first.
4. Harvest (batch gate-answering)
Triggered when the architect returns and parked.md exists.
Serve the artifacts if present. Check <flightDir>/artifacts/ — if any .html files exist, serve them through the /proto-buddy skill pointed at that directory on port 3333. proto-buddy self-locates its own scripts; do not hardcode plugin paths — they differ across cache and marketplace installs and resolve only inside a /proto-buddy invocation (this is why explore-latent's recipe is skill-scoped too). Each .html becomes a live tab; triad calls happen against the running tab, not a static file.
Present all parked gates. Do not batch-hide any. Every gate in parked.md is surfaced.
Collect decisions. For each gate — present what_youll_know, decision_criteria, inviolate_touched, options (label + summary), and the artifact tab URL. Ask the architect to type predicted / observed / gap as plain text in the conversation before picking. Read the three fields from their reply. Once you have the triad, use AskUserQuestion (up to 4 gate picks per batch) presenting the option labels with summaries. No bare approval — the typed triad is mandatory; the friction is the product. Build each resolvedGates[] entry from the conversational triad + the picked label.
Collect answers for all parked gates across as many batches as needed (4/batch max).
Re-arm. Build resolvedGates[] — one entry per answered gate:
{
"node": "<node-id>",
"pick": "<chosen option label, exactly as in gate.options>",
"predicted": "<text>",
"observed": "<text>",
"gap": "<text or 'none'>"
}
Fire the engine again with resolvedGates and state loaded from state.json. Set a new startedAt. Set a new heartbeat wakeup.
Stop proto-buddy after re-arm fires. Shut the server down through the same /proto-buddy skill that started it — it owns the server lifecycle. No raw plugin paths.
Return to step 3.
A+ routing (why some gates ask and some don't)
The build runs autonomous with parked forks:
- Cheap gate (reversible) — engine spawns a decision agent in-run, forced by schema to log
predicted / observed / gap before picking. Does not stop. Reasoning appears in checkpoints afterward.
- Expensive gate (irreversible, touches an inviolate) — engine parks it, queues artifact + notify side effects, continues flying all independent branches. Only returns when nothing can fly unaided.
The engine grinds the independent branches while expensive gates wait. You see all parked gates at once and answer in batch.
The mvt ceiling
Every leg carries its mvt — the minimum viable test. The engine treats it as a hard ceiling: the smallest artifact that yields signal is the whole job. stats.overruns surfaces legs that elaborated past it. The depth trap (polishing one branch instead of testing many) is the known failure mode this guards.
What this skill refuses
- To fly a prose-only explore-latent output — send it back for DAG mode.
- To auto-decide an expensive gate — irreversible forks always come to you.
- To accept a bare approve at a gate — typed predicted/observed/gap is mandatory.
- To re-rank one path five ways — strategy lives in gate-0, not in re-ordering a chain.
- To fire a first overnight flight on a DAG that has not passed a
dryRun rehearsal. The rehearsal proves the DAG is well-formed and the wave loop terminates before you leave it unattended. Run dryRun: true first; get a COMPLETE or NEEDS_INPUT from the dry run; then arm for real.