Appearance
Pipeline internals
This page documents the story pipeline for operators and contributors: the stages, which actor runs at each, how the orchestrator routes between them, and the recovery paths. The canonical sources are src/core/state-machine/decide-transition.ts (the transition truth table) and docs/ARCHITECTURE.md §5 — refer to those when extending the machine.
The linear walk
Think → Plan → [APPROVAL] → Build → Review → Test → Ship → ⟶ [PR outcome] ⟶ Reflect| Stage | Actor | Inputs | Output | Human gate |
|---|---|---|---|---|
| Think | Architect (Think turn) | ADO story + project memory (<repo>/.vlx/memory/) | stories/<id>/spec.md (problem, AC, constraints, open questions) | only if it asks a question |
| Plan | Architect (Plan turn) | frozen spec.md | stories/<id>/plan.md (file list, AC traceability, test approach, docs impact, rollback) | no |
| APPROVAL | none (gate) | plan.md | approve / reject | optional — off by default (approval_gate.enabled) |
| Build | Builder | spec.md, plan.md | code + tests + docs committed on vlx-bot/<id>; .vlx/<id>/build-result.json | only if it needs a credential or asks |
| Review | Critic | diff vs base, plan.md | stories/<id>/review-findings.json (strict) | no |
| Test | Inspector | spec.md, plan.md, review-findings.json | stories/<id>/{qa-plan.md, qa-evidence/, qa-result.json} | no |
| Ship | none (orchestrator) | branch + all artifacts | PR opened, ADO story linked, Telegram status posted | no |
| Reflect | Reflector (async) | event log + artifacts + PR-status events | stories/<id>/reflection.json (ground-truth signals + proposed corrections) | no |
intake (the queue-claim step) is not a pipeline stage — it's how the orchestrator claims a story off the queue.
Routing & outcomes
Each stage emits a structured outcome; the orchestrator's MECE transition table (decide-transition.ts) maps (stage, outcome) to the next move. Outcomes:
| Outcome | Meaning | Typical routing |
|---|---|---|
completed | stage produced a valid artifact | advance to the next stage |
deferred | awaiting human input (a question fired) | suspend; resume on the human's reply |
failed | the stage could not complete | terminal-fail (subject to auto-escape) |
escalated | fail-closed: the harness can't trust the result | needs human review |
plan_inconsistent | implementation diverged from the approved plan | re-plan → re-enters APPROVAL |
coverage_incomplete | a declared AC has no test coverage | one Build fix-up attempt on its own budget |
mockup_mismatch | implemented UI doesn't match an attached mockup | one Build fix-up attempt, then flag-and-ship |
Fix-up budgets. coverage_incomplete and mockup_mismatch each grant a single Build retry on a dedicated budget; if still unresolved the run ships flagged (loud warning in Telegram + ADO + PR body) rather than terminal-failing — a human judges it at PR review.
Deviation → re-plan. If Build deviates from the approved plan, the run re-enters Plan, which re-enters the APPROVAL gate — the revised plan returns to the human before further code changes (posted non-blocking so the operator is informed without stalling).
Post-Ship: event-driven, not linear
The linear walk ends at Ship (PR opened). After that, the PR-status poller (src/daemon/pr-status-poller.ts) polls each tracked PR every 60 s and emits events:
| Event | Handler | Action |
|---|---|---|
pr_status_changed (vote) | daemon | Telegram nudge + a /respin hint — votes do not auto-respin |
pr_comment_added | Responder actor | one-shot read-only classification → in-thread reply |
| 10-min comment quiescence | maybeAutoRespin | new Build run addressing the whole comment batch; push to the open PR |
merged / abandoned | reflect handler | un-track PR, record outcome, run Reflector → reflection.json → [brain] PR |
Per-comment Responder. Each new reviewer comment triggers a one-shot, read-only turn that classifies it (question / change_request / nit) and replies in-thread: questions get a factual answer; change requests get an agree/disagree reply (agreed ones feed the respin); nits are skipped. Replies are bot-tagged so the poller never re-triggers on them, capped at 3 automated replies per thread.
Debounced auto-respin. Once a PR has respin-eligible comments (unresolved threads the Responder agreed to change) and the reviewer has been quiet for 10 minutes (RESPIN_DEBOUNCE_MS), the poller auto-triggers a respin. New comments reset the window; an active run blocks re-fire; /respin <storyId> bypasses the wait.
Fast-path profile
Every story pays the full six-stage pipeline by default. For small or docs-only work the operator can collapse and trim the pipeline with ADO tags — a deterministic, tag-gated profile. No LLM decides the path (profile resolution is a pure function of the story's tags, the same zero-model style as the Designer/Sentinel triggers), and untagged stories are byte-for-byte unaffected — they resolve to the full profile and every stage behaves exactly as above.
| Tag | Effect |
|---|---|
vlx-small | Think + Plan collapse into one Architect session (produces both spec.md and plan.md, both verified in that turn); the normal APPROVAL gate is kept; Build is kept; Review runs as a shallow single pass (the /codex cross-model second opinion is skipped); Test is kept. |
vlx-skip-review | The Review stage is bypassed entirely. |
vlx-skip-test | The Test stage is bypassed entirely — for docs-only / non-code changes only (no gates run). |
Tags are case/bracket-insensitive (vlx-small, VLX-Small, [vlx-small] all match) and independent — the skip tags work with or without vlx-small (e.g. a docs-only story tagged only vlx-skip-test). Tags are frozen at intake, so the profile is stable across the whole run.
No new outcomes. Every fast-path effect resolves to an ordinary completed outcome — the decide-transition.ts table is unchanged. vlx-small collapses work into the Think stage (plan.md is committed in the Think checkpoint, so recovery is safe; if it's absent post-restore the Plan stage regenerates it via the normal turn). The skips short-circuit their stage before the actor runs.
Recorded and disclosed. Every skip appends a stage_skipped audit event to the run, and the Ship stage prepends a Pipeline profile section to the PR description — naming the profile, the combined Think+Plan note, the review depth, and a prominent ⚠ line for any skipped stage (Test SKIPPED … no gates ran). The Think handler records the resolved profile once via a pipeline_profile_resolved event.
APPROVAL is kept, not forced. vlx-small keeps the normal approval gate (governed by approval_gate.enabled); it never removes approval and never forces it on when the project has it globally disabled. Operators using the fast-path are advised to keep approval_gate enabled — it is the one human checkpoint the profile preserves.
A global kill-switch (fast_path.enabled, default true) ignores all three tags when set to false. See Configure — fast_path.
Operator commands & auto-escape
Telegram slash commands (src/daemon/telegram-commands.ts):
/respin <storyId>— respin open-PR comments now (bypasses the debounce)/requeue <storyId>— revive a failed story from where it died, fresh retry budget/restart <storyId> <stage>— re-run from an arbitrary stage/cancel <storyId>— kill the active run (excluded from auto-escape)/history <storyId>— last 30 events of the story's latest run
Auto-escape (src/core/state-machine/auto-escape.ts): a pre-Ship failure lands the story in queue state failed, which no claim loop re-picks. A daemon sweep autonomously requeues such stories up to 2 times per story (counted via auto_escape_triggered events), skipping cancelled runs, already-shipped stories, and stories whose ADO work item is no longer New/Active. At the cap the run is marked needs_review and Telegram is notified once.
A session-limit rate limit is excluded from this budget entirely — it pauses the run (waiting, auto-resumes) rather than failing it. See Run a story — Rate-limit pauses.
Artifact convention
Path helpers live in src/core/paths.ts:
<worktree>/stories/<id>/— durable story artifacts (spec, plan, review-findings, qa-*, reflection); part of the story PR. No per-attempt segment — retries reuse the same folder.<worktree>/.vlx/<id>/— gitignored intermediate state (build-result.json handoff, scratch, per-stage checkpoint manifests).- Branch
vlx-bot/<id>— one branch (and worktree) per story; retries resume from its tip. - Client-repo project memory at
<client-repo>/.vlx/memory/, committed.