VgScript agent guide
The IDE-agnostic reference for AI agents working in a VgScript content repo: the language, the vgs commands, and the conventions to follow.
On this page
This is the single, IDE-agnostic guide for AI agents (Claude Code, Cursor,
Copilot, etc.) working in a VgScript content repo — a project full of .vgs
scripts authored with this plugin.
Is the same guide valid in every IDE? Yes. The VS Code extension and the JetBrains plugin are thin wrappers over the same
vgsengine and the same grammar (both live incore/). Everything an agent needs — the language and thevgsCLI — is editor-agnostic, and agents drive the CLI directly rather than clicking menus. Both plugins even expose the identical command set (Fix + Reconcile,Validate,Fix + Reconcile All,Resolve Links,Reorder Scenes,Travel) under aVgScriptmenu, so the human-facing steps match too.How to use it: use the public
vgscript-exampleproject as the reference for your content repo'sAGENTS.md(orCLAUDE.md). The Language and Tooling sections below are universal — copy them verbatim. Only the Project context section is per-project; fill it in.
Project context (fill this in for your repo)
This section is a placeholder. Replace the prompts below with your project's own details, but keep the headings so agents always know where to look.
- Premise — one or two sentences on what the game/story is about.
- Authoring language — the natural language the prose is written in (and any
rule, e.g. "keep tags in English, prose in
<language>"). - Conventions — anything non-obvious about tone, naming, or structure an agent should preserve.
Workspace structure (typical — adapt to your repo)
- Scenes are
.vgsfiles, each with a matching<file>.vgs.metasidecar. - Scenes are usually grouped into directories (acts, chapters, etc.) and numbered
sequentially, e.g.
S1_Intro.vgs. Scenes.txtat the project root lists scenes in play order; severalvgscommands read it.
The vgscript language
vgscript is a lightweight bracket-tagged DSL for narrative scenes: dialogue,
named actions, timed/parallel events, and branching decisions. Every tag sits at
the start of its line, in square brackets. The descriptions below match what
the vgs parser actually does (core/tool/src/reconcile.rs); where a feature is
interpreted by the game engine rather than the tool, it is marked
(engine-level).
Entries vs. context tags
The tool parses a script into entry groups. Only four tags open an entry:
| Tag | Entry | Notes |
|---|---|---|
[A] | Action | Engine actions/events. Often a scene's SceneHandler (an action called via events/triggers). |
[T] | Talk | A block of visible narrative / dialogue lines. |
[D] | Decision | A player choice with numbered options. |
[C] | Condition | A condition block that gates flow (engine-level). |
Two more tags carry context but do not open an entry of their own:
[S]— Scene header. Defines the scene key + name. Written as either[S1_Intro](key/name split on the first_) or[S1] Intro(name after the bracket). With neither, the key/name fall back to the filename.[P]— Speaker (persona). ⚠️ This is who is speaking, not the spoken text. The text after[P]names the character; an arrow names who they talk to, one or several split by commas:[P] Hero,[P] Hero -> Villainor[P] Hero -> Villain, Mentor. The speaker is attached to the following[T]talks and[D]decisions ascharacterin the.meta, and the listeners to the talks only, as thedirectedTolist. An empty[P]clears the speaker. The actual lines of dialogue live in the[T]block beneath it.
Sublines
Numeric sublines [1] [2] [3] … belong to the most recent parent entry above
them. A subline may also be written in long form [D1-2] — reconcile normalizes
it to [2]. [X] (or [x]) is a placeholder/first subline.
[P] Hero[T1][1] So this is where it begins.[2] Nothing here yet. Only my voice.
Flow control and timing
- Sequential —
->(X): after this line/action finishes, go to entryX. Example:[A1] Intro ->(A2). - Parallel —
&(X): startXalongside this line/action without waiting for it (like a shellcmd &). Example:[A1] FadeIn &(Timer{S}). - Cross-script jump —
->(Sx-Code): jump to an entry in another scene. Runvgs resolve-linksto replace these with stable destination hashes (see Tooling). - Everything after
->or&(on a line is flow, not visible text — the tool excludes it when computing a line's displayed text.
The following are engine-level conventions. To the vgs tool, any {…} is an
opaque inline tag that it strips from visible text; the tool does not validate
their meaning:
- Timers —
Timer{XS},Timer{S},Timer{M},Timer{L}: engine-defined durations (extra-small … large) for easy global tuning. - Events — an action that calls a sub-action by name, e.g.
[A2] Event{A1, "UnlockDoor"}("A2 calls theUnlockDoorevent from A1"). - Inline markers — e.g.
{!repeat}: metadata on the line, stripped from the visible/.metatext. - Magnitude suffixes —
XS, S, M, Ldenote size/scale for timers, text blocks, and other magnitude-based parameters.
Decisions and branching
A decision lists numbered options; each option's arrow says what to start when chosen:
[D1][1] Accept ->(A_yes)[2] Decline ->(A_no)
The tool understands two things inside options:
- Copy text from a talk —
=(Tn-1)pulls the visible text of[Tn]line[1]into the option. Reconcile materializes it: a line like[1] {!repeat} =(T1-1) 'Old text' ->(T1)is rewritten to[1] {!repeat} 'Actual talk text' ->(T1), and the.metastores the resolved text. Author-written quoted/plain text that you didn't reference is left alone. - Conditions — an option may start with
if(...); theif(...)is not counted as visible option text. (Broader decision queries such asDecisionsCountLess{...}and=(Dn-x)echoes are engine-level.) Terms combine with&&and||(&&binds tighter, no parentheses), e.g.if(D2-1 || D2-2)orif(T5 && HasFact{met} || T6); a comma still means&&.if!(...)negates the whole expression (engine-level).
Writing decisions that feel like decisions
A decision's job is to give the player a sensation of agency — the feeling they are authoring their own route — even when every branch reconverges on the same beat. The narrative can (and usually should) funnel back together; what must not collapse is the felt difference between options at the moment of choosing.
The failure mode to avoid is synonym choices: two or more options that say the same thing in different words. They read as a fake decision and break immersion.
✗ Anti-pattern:
[D1][1] We should leave.[2] It's time to go.[3] There's nothing left for us here.All three are the same intent reworded, and the flow merges regardless. There is no real choice here.
Make options differ in kind, not just wording. Good axes of divergence:
- Attitude / value — pride vs. doubt, tenderness vs. cruelty, awe vs. contempt. The character reveals who they are by what they pick.
- Approach / method — what they'll do next differs, even if it lands in the same place (get past the guard by force vs. by persuasion).
- Tone / register — clinical vs. poetic, defiant vs. resigned.
- What they notice — each option fixes on a different facet of the scene.
When branches must reconverge, let the choice still leave a trace: a changed line later, a remembered stance, or a different transition into the shared beat. Reconvergence is fine; an invisible choice is not.
Quick self-check before committing a [Dn]: if I swapped two options, would the
player notice a different meaning, mood, or consequence? If not, rewrite or cut
until each option is a genuinely distinct stance.
Actions
- Actions are meant to be reusable. Ask the user before adding a new one.
- Parameters are usually key-value pairs separated by colons; keep the syntax consistent with existing cases.
- A scene normally has at least one
SceneHandleraction — an action designed to be called via events and triggers.
Comments
// line comment— to end of line./* block comment */— may span lines.
Both are stripped before parsing, so they never affect entry text or hashes.
Anchors and references (tool-managed — never hand-write)
{#guid:…}— a stable per-entry ID the tool injects so references survive renumbering.{#dst:<guid>/<hash>}— legacy destination metadata;resolve-linksreplaces the scene-code reference and this tag with the destination hash.- A bare 32-char hex hash gets annotated
/*Scene Code*/(or/*MISSING*/if the target is gone). Leave these to the tool.
Meta files
Each .vgs has a <file>.vgs.meta (JSON) holding scene key/name and per-entry
records (currentCode, stable hash, visible lines, the character of talks and
decisions, and the directedTo list of talks). Never hand-edit .vgs.meta —
edit the .vgs and reconcile.
Tooling: the vgs CLI (how to "reconcile")
Do not write your own parser or fix scripts, and never hand-edit .vgs.meta.
All .meta generation, entry renumbering, decision-text filling, cross-script
link resolution, and validation are owned by the bundled vgs CLI. Drive it.
What "reconcile" means
Reconcile = regenerate a script's <file>.vgs.meta so it matches the .vgs
source. One pass:
- Parses the script into entry groups (
[A] [T] [D] [C]with numeric sublines). - Preserves stable per-entry IDs.
vgs fixrenumbers the script first and reads the old.metain the new numbering. Each entry keeps its ID by matching, in order: a{#guid:…}anchor → its code and content (text with parameters, without flow arrows) → its content alone, wherever it moved → its code, for an entry edited in place. A deleted entry's ID only goes to an entry with exactly its content, never to a new entry that lands on its number. Content alone only moves an ID when no other entry shares it, so flow-only entries (Node ->(…),Execute{A1} &(…)) keep their IDs by code. A new ID is minted only when nothing matches. This is why adding, removing or renumbering entries does not break references. - Auto-fills decision options that reference a talk via
=(Tn-1). - Annotates cross-script hash references (
/*Scene Code*/or/*MISSING*/). - Writes/updates
<file>.vgs.meta, and may lightly rewrite the.vgsitself (inject{#guid:…}anchors, normalize sublines to numeric).
Workflow is always: edit the .vgs, then reconcile to regenerate the .meta.
Adding and removing entries
Add and remove actions, talks, decisions and conditions wherever the story needs
them, including in the middle of a scene. Renumbering is the tool's job and costs
nothing (vgs 0.2.21 or later; check with vgs --version). A new entry that uses an
existing action needs no approval; only a new kind of action does (see Actions).
- Write the new entry where it belongs in the flow, with a number its kind doesn't
use yet in that scene (
[A50],[T90],[D9],[C9]), and point the arrows at that number.vgs fixrenumbers the scene in file order, rewrites every reference to the new codes (flows,[C]lines,if(...), parameters such asEvent{A5, …}orTimer{M, T3}) and keeps every existing entry's ID. - The number only has to be unused. A code that is already taken (two
[A3]entries) makes the references ambiguous, and the renumbering comes out wrong. - Removing works the same way: delete the entry, repoint the arrows that led to it,
run
vgs fix. Only the deleted entry's own ID disappears; search the scenes for its hash and repoint any cross-script link to it. - Cross-script links store IDs, not codes, so renumbering never breaks them.
- Never bend a scene to avoid renumbering: don't fold a new step into an existing entry, park it at the end of the file, or drop one of the project's conventions to keep the current codes.
- To link to a new entry from another scene, run
vgs fixon its scene first, then write->(Sx-Code)in the caller and runvgs resolve-links.
How to run it
In the IDE (JetBrains or VS Code, identical): right-click the .vgs (or a
folder, for the project-wide actions) → VgScript → Fix + Reconcile (or
Fix + Reconcile All (Scenes.txt) for the whole project).
From a terminal (agents: do this): vgs is the compiled binary the plugin
drives. Installing the JetBrains plugin or the VS Code extension is enough.
JetBrains copies its bundled vgs to one fixed location when it opens a project,
and VS Code when it opens a .vgs file, whichever IDE or plugin version you have:
| OS | Installed CLI |
|---|---|
| Windows | %LOCALAPPDATA%\vgscript\bin\vgs.exe |
| macOS / Linux | ${XDG_DATA_HOME:-~/.local/share}/vgscript/bin/vgs |
Find it in this order and use the first that works:
vgsonPATH. Runvgs --version. The JetBrains plugin offers to add the folder above toPATH, and Settings → Tools → VgScript has an Add to PATH button.- The installed CLI path above, e.g. in PowerShell
& "$env:LOCALAPPDATA\vgscript\bin\vgs.exe" --version, or in Git Bash"$LOCALAPPDATA/vgscript/bin/vgs.exe" --version. - Inside the plugin's own source repo only:
core/tool/target/release/vgs[.exe].
If none exists, ask the user to open the project once in an IDE with the VgScript
plugin installed. Don't build or reimplement the tool yourself. The bundled copy
is compiled for one platform per release, so on another OS the user may need to
build vgs and put it on PATH (or set Settings → Tools → VgScript,
VS Code vgscript.rustBinaryPath).
| Goal | Command (alias) |
|---|---|
Reconcile one script (renumber, fill, (re)write its .meta) | vgs fix <file.vgs> |
Reconcile every scene, in Scenes.txt order | vgs fix-all [dir] |
| Find mistakes in one script | vgs find-mistakes <file.vgs> (vgs validate) |
| Find mistakes across all scenes | vgs check-all [dir] |
Replace cross-script Sx-Code links with destination hashes | vgs resolve-links <dir> |
Reorder scenes from Scenes.txt (add --rename-files to rename to S<n>_<Name>.vgs) | vgs reorder-scenes <dir> [--dry-run] [--rename-files] |
Walk every jump from an entry and report where the branching gets stuck (loose-ends) | vgs travel [dir] [--from S1-A1] [--only dead-ends,…] |
| See what the editor underlines in one script: jumps, lines, scenes or IDs that do not exist, codes written twice, unpaired brackets | vgs analyze <file.vgs> [--json] |
[dir] defaults to the current directory; pass the folder containing
Scenes.txt. vgs fix is the editor's "Fix + Reconcile" (it enumerates, then
reconciles with subline normalization) — prefer it over the lower-level
enumerate / reconcile / batch subcommands. Run vgs --help,
vgs <command> --help, or vgs help-all for the full spec.
Typical workflow after writing or editing a scene
vgs fix <file.vgs>— normalize entries and (re)generate<file>.vgs.meta.vgs find-mistakes <file.vgs>— confirm it is clean.
For a whole-project pass use vgs fix-all then vgs check-all.
If you renamed an entry's code by hand ([T3] to [T7], and every reference to
it), tell fix so the entry keeps its ID: vgs fix <file.vgs> --renamed T3=T7
(several: --renamed T3=T7,A2=A5). The editors do this for you when you rename
an entry with their Rename command.
After changing branches or jumps, run vgs travel [dir] (add --from S3-A1 to
start somewhere other than the first action of the first scene). It follows every
->, every decision option and every condition branch without evaluating any
condition, and reports what can stop a playthrough: a talk, option or branch with
no ->, a jump to an entry that does not exist, a [C] without else, a [D]
whose every option has an if(...), and loops nothing leads out of. It also lists
the endings it reached (actions with no ->) and the entries nothing reaches.
To check one thing, narrow it with --only, for example
vgs travel --only dead-ends,broken-links: the report keeps those kinds, the travel
keeps only the paths to them, and the exit code is 1 only if one of them is found.
--only problems keeps every problem; vgs travel --help lists the parts.
Exit codes (branch on these instead of parsing output)
0— success / no problems found.1— validation problems found (find-mistakes,check-all), ortravelfound a place where the branching gets stuck.2— the script orScenes.txtcould not be found.
Golden rules for agents
- Edit
.vgs, never.vgs.meta— letvgs fixregenerate the sidecar. - Don't reimplement the tool — no hand-rolled parsers or "fix" scripts.
- Reconcile, then validate after any edit (
vgs fix→vgs find-mistakes). - Ask before adding a new action; reuse existing ones.
- Keep style: descriptive action names, readable indentation, consistent tag casing, no trailing spaces on tag lines.
- Make decisions genuine — distinct in kind, not synonyms (see above).
- Insert and delete entries freely — give a new entry any unused number where
it belongs;
vgs fixrenumbers the scene and keeps every ID. Never restructure a scene to avoid renumbering (see Adding and removing entries).