Drafts
A draft gives a session its own copy of one or more repositories. Vis then changes files there, not in your checkout. Each copy is a Git worktree or a copy-on-write clone. Drafts are experimental and off by default.
When to use
- You want Vis to try a change without editing the files you are working on. Vis makes the change and runs its checks in its own copy. Enable drafts explains what that protection covers.
- You want to read the whole diff before anything reaches your branch. Review the changes, then approve them or ask Vis to discard the draft.
- One change spans several repositories. Include each repository in the draft without switching projects.
- A session is working in the wrong draft. Ask Vis to return it to its original checkout.
- Your team needs its own check before a draft is approved. An extension can guard approvals with a hook.
- Your own program must turn on drafts or read the draft state. Use Drafts API.
Read-only questions and analysis do not need a draft.
Enable drafts
Open Settings in the terminal or the app. In the Experimental section, set Draft
backend to auto, worktree or rift. To stop new drafts, choose off.
With drafts on, Vis does each task that changes files in a draft of its session. You do not have to ask for it. This covers code, tests, documentation and configuration, and the checks run in the draft too. If Vis cannot create a draft, it tells you why and leaves your checkout alone. Name every repository that the task must change.
Draft write protection does not need the jail. While drafts are enabled, the file tools of Vis refuse writes to the original repositories, with or without the jail. The jail adds OS-level confinement for shell children and the Python sandbox. Without it, a raw shell command can still reach your checkout. If you change the draft backend while a Python context is open, start a new turn. Vis then rebuilds that context safely, and Python variables do not carry over to it.
Ask Vis to work in a draft
Fix the parser and run the tests, then show me the draft's diff. Do not commit or push until I approve.
The setting controls whether drafts are enabled. Only the agent creates, approves and discards them. There is no draft-management menu or slash command in the TUI or Companion app. You control those actions through the conversation.
Approval commits and may push. draft_approve() commits the draft's changes,
fast-forwards the default branch, restores unrelated local work and pushes to
origin when configured. Enabling drafts does not authorize commits or pushes:
Vis needs your request or permission in your project instructions. Ask to review
first to keep the draft unapproved. Approval leaves it open for further work.
When your task is complete, Vis checks the draft. It must have no pending changes, and all its
commits must be merged into the target branch. Any required push must have succeeded. Vis then
removes the completed draft without asking again. For your next task, Vis starts a fresh draft from
the latest origin target branch. With no remote configured, it starts from the committed local
target.
Discarding an unfinished draft can lose unapproved changes or unmerged commits, so Vis asks for confirmation first. Drafts awaiting review or a required push stay open. Work already merged stays on the target branch.
See the draft state
The terminal footer shows the draft name and its counts:
CHANGES: the files in the task review.PENDING: changed paths that are not committed.UNMERGED: draft commits that the target branch does not have.
After approval, PENDING and UNMERGED go to zero when the changes land locally. These counts do
not show if a push worked. When Vis discards the draft, the footer shows your checkout again.
Recover a session opened in the wrong draft
Ask Vis to check the draft state and return the session to its original checkout.
If the session inherited another draft's directory, draft_status() reports
recovery_required. draft_discard() then returns the session to the source
checkout without removing that draft or its files. The result names the
preserved_root.
If Vis cannot find the directory's ownership record, it will not guess what to
delete. Use /cd /path/to/original-checkout to return the session to a checkout
you recognize. The unrecognized directory and its contents stay untouched.
Work in another repository
You can isolate a change across added repositories without switching projects or changing their configured draft policies:
Update the parser and its client together in a draft. Leave both original checkouts unchanged, and show me each repository's diff before approval.
When your configuration exposes sibling_path, select both repositories:
print(draft_create("parser-and-client", roots=[project_root_path, sibling_path]))
The first entry becomes the primary repository: from the next block,
project_root_path points to its draft. Added-root Path variables follow their
working copies too. To work only in the sibling, use roots=[sibling_path].
Omitting roots preserves the project's configured copy policies.
Every selected source must be an existing, authorized repository root that your session can read and
write. You can select a shared root without changing your configuration. You cannot select
read-only, copy-only or not-allowed roots, or roots that overlap filesystem deny rules. Vis
refuses empty lists, duplicates and nested selections. It does not guess. Roots that you do not
select keep their configured policies.
Draft review and approval do not include shared roots. A session has one active draft group at a time. Approval needs authorization for all participating repositories. Discarding returns to the original project.
Review changes before approval
Ask Vis to attach the draft's diff, or to include a diff with each completed task's implementation report. Open it in Companion or the TUI to read file changes and comment on them. Sending a review round does not approve the draft or publish code.
A diff compares immutable snapshots. Vis takes the first snapshot after it creates the draft. So
changes copied with clean=False are part of the starting point, not new implementation work. Later
changes to the original checkout cannot change that baseline. Worktree and Rift drafts both use this
review path. It does not change the Git index, branches or commits of either working copy.
Copied source work is excluded from the task-only review, not from approval. It remains in the draft and can overlap pending changes in the source checkout. Approval refuses those overlaps. Copying is not permission to overwrite them. Status distinguishes task review counts from pending Git changes.
By default the diff covers all changes since the draft began. A checkpoint lets the next diff show only the following task, including later edits to the same files. The final cumulative diff still uses the original starting point. File contents, additions and deletions come from the draft. Comments are saved separately and never rewrite the patch. Ignored untracked files and backend bookkeeping are not review material.
Approval
The target is the local branch named by origin/HEAD. Without that symbolic ref, Vis selects
main, then master. If the target does not exist locally, approval refuses. It does not pick
another branch. Vis never switches the branch in the original checkout.
If origin is configured, approval fetches the target from it. The draft must contain both the local
target and that fetched commit. If it does not, approval refuses with :draft/sync-required.
Then ask Vis to run draft_sync() with commit permission. It saves pending draft work and merges
the target history inside each draft. It does not approve or push. Approval itself never merges
target history.
If synchronization reports conflicts, edit the reported files in the affected
draft, then call draft_sync(action="continue"). To cancel that owned merge, use
draft_sync(action="abort"). An optional roots=[...] restricts synchronization
to selected participants. Vis refuses unrelated in-progress Git operations rather
than taking ownership of them. Do not bypass a refusal with raw Git recovery.
Pending draft work is committed on vis/<name>, excluding backend bookkeeping.
The subject defaults to draft(<name>): approve. Commits include Vis-Session
and Vis-Draft trailers. Local landing is fast-forward only. A linked target
checkout is updated in place. An unchecked-out branch is updated by compare-and-swap.
The target checkout can be dirty. Overlapping paths are conservatively refused
before any stash or landing, even when edits affect different lines in one file.
Unrelated staged, unstaged and untracked work is saved in an approval-owned stash,
then restored with --index. Existing user stashes are preserved. Ignored files
are not stashed and cannot be overwritten by landing.
Push to origin happens only after successful restoration, without force. With no
origin, approval is local only. The result includes published, branch,
target_branch, commit and files. nothing-to-approve means the draft and local
target already match with no pending changes. It still retries publication.
Publication across repositories is not atomic. Preflight checks cover all participants, but a later commit, restoration or push can fail after an earlier repository lands. Inspect the per-repository results: successfully landed history is not rolled back, and the draft stays pinned for recovery and retry. Already published repositories do not need new approval commits on retry.
If restoration fails, Vis does not push that repository and keeps its saved stash. Approval stays blocked while recovery is needed. Keep the stash and follow the reported blocker. Do not delete the stash to force a retry.
If the push fails after landing, Vis has already restored your local work. Synchronize if needed, then retry approval. Normal Git push rules reject the push if the remote branch moved. Failed commits and extension vetoes are failures, not successful approvals.
Backends
| Backend | How the draft is made | Where it lands |
|---|---|---|
worktree |
git worktree add from HEAD. By default, pending work is not included. |
Fast-forward the shared target, restore local work, then push to origin if configured. |
rift |
Copy-on-write clone, reset to committed HEAD by default |
Fetch the draft branch into the original repository, then use the same approval flow. |
worktree needs a git repository with at least one commit. rift works in any
directory with clean=False but needs the Rift native library. Clean drafts
require Git history. The draft_backend toggle chooses between them:
| Value | Meaning |
|---|---|
auto |
Require drafts for changes. Use worktree when the repository allows it, else rift. |
worktree, rift |
Require drafts that use only that backend. draft_create refuses when the backend is unavailable. |
off (default) |
No automatic draft workflow or draft creation. draft_create explains why. |
Set it in Settings → Experimental, or in ~/.vis/config.yml:
toggles:
draft_backend: worktree
Draft working copies live under ~/.vis/drafts/. Other roots follow their own draft policy from
the filesystem_roots configuration. See
Configuration. Copy-only dependency roots keep their
source bytes, even with clean=True. They are never approval targets. An explicit selection cannot
make a copy-only root part of the approval.
Reference
Vis uses these sandbox tools for drafts. You do not call them yourself. Ask Vis in the conversation.
Draft tools
| Tool | Effect |
|---|---|
draft_create("name", clean=None, roots=None) |
Open a draft from committed HEAD. To select repositories, pass a nonempty list of authorized root Paths. The first one is primary. Omit roots to use the project and configured copy policies. clean=False also copies pending source changes. One draft group at a time. |
draft_status() |
Report each repository, target branch, unpublished commits and pending paths, plus task-only review counts. |
draft_diff(...) |
Attach a reviewable diff of the draft's changes and return a checkpoint for the next task. No commit, approval or push. |
draft_sync(action="start", message=None, roots=None) |
Fetch and merge target history inside the draft. Resolve reported conflicts, then use action="continue". To undo this synchronization, use action="abort". It can create commits, so it needs commit permission. |
draft_approve() |
Commit pending work, fast-forward each default branch and publish to origin if configured. draft_approve("subject") sets the subject. The draft stays open. |
draft_discard() |
Return to the original checkout and remove the draft working copy. Approved work stays on the default branch. Unapproved changes are lost. A merged draft branch can be removed. |
Opening or discarding a draft updates the session's workspace immediately. Use
session["workspace"] and the Path bindings from the next block, when Python's
writable paths are refreshed.
While in a draft, session["workspace"]["draft"] includes label, backend,
branch, target_branch, approved_ahead and pending_paths.
Diff tool
draft_diff(filename=None, since=None) attaches DIFF-<draft-label>.json for one
repository. Its result includes the attachment descriptor, a checkpoint snapshot
tree ID and an empty flag. With several repositories, it creates separate
-repo-1.json, -repo-2.json attachments. Each document identifies its source
repository, so identical file paths cannot be confused. The result includes an
attachments list and a checkpoint map keyed by source repository path.
Keep the complete checkpoint value for the next task. Use a different filename for each task and one stable filename for revisions of that task's diff:
first = draft_diff(filename="DIFF-search-task-1.json")
print(first)
# After completing the next task:
second = draft_diff(filename="DIFF-search-task-2.json", since=first["checkpoint"])
print(second)
# The whole implementation, still measured from the draft's starting point:
print(draft_diff(filename="DIFF-search.json"))
A checkpoint must have been recorded by this draft. Multi-repository checkpoints must contain every member. Missing, foreign or invalid members are refused before any attachments are created. Snapshot IDs are not Git commits. A diff with no changes still records its checkpoint and clearly reports that it is empty. The tool does not approve the draft, commit, push or run an implementation.
The snapshot store uses Git even for a Rift directory without Git history. It is private to the draft and removed when the draft is discarded. A draft created without a saved baseline cannot supply a trustworthy historical diff. Vis refuses rather than using today's original checkout as yesterday's starting point.
Hooks for extensions
Every create, synchronization, approval and discard goes through draft/create,
draft/sync, draft/approve and draft/discard, so a Python extension can guard
or observe them with vis.OpHook. A before hook returning vis.block(reason)
stops the operation. Each approval-created draft commit also crosses
git/commit. Git's own hooks are not bypassed. See
Extension API.
See also
- Drafts API — turn on drafts and read the draft state from your own program.
- Configuration — the
draft_backendtoggle and thedraftpolicy of extra roots. - Extending Vis — op hooks on
draft/create,draft/approveanddraft/discard.
