Skip to content

Build Errors Explained

Every error carries a file:line pointer. Warnings don’t stop the build; errors do.

MessageCause / fix
expected "=== fragment_id" before contentProse or directives before the first === line.
fragment id "…" must be snake_caseIds are lowercase letters, digits, underscores, starting with a letter.
duplicate fragment idTwo fragments share an id (in one file, or across files - the cross-file check runs in npm test and at load). Rename one.
defines N fragments … a shipped file holds exactly oneSplit them, one file per fragment. Only files the build ships are checked, so an underscored draft can hold as many as you like.
holds fragment "…", so it should be named "….frag"The filename and the id have to agree. Rename whichever is wrong.
unexpected line in fragment headerInside the header (before the first blank line) only phase:, title:, tags:, requires: are allowed. Usually a typo’d field name, or a missing blank line before the prose.
missing "phase:" / missing "title:"Required header fields.
unknown phase "…"phase: must be one of the six life phases.
expected "@ subscene_id" or "==="Loose content after a fragment’s scenes ended.
duplicate subscene idTwo @ scenes with the same name in one fragment.
MessageCause / fix
"*requires hidden:" is one word nowUse *requires_hidden:. One directive is one token, so there is no whitespace inside one.
"==" is not an operator hereConditions compare with a single =. There is no assignment inside a condition to tell it apart from, so there is no second spelling.
cannot parse condition term "…"The term matches no known form. Check the grammar.
cannot parse check "…"Dice checks look like random(20) + VIT >= 70.
random(N) needs at least 2 sidesA one-sided die isn’t random.
random() is not allowed in inline {if}Inline text re-renders; roll in an *if block or *set: x = random(N) first.
unknown stat "…"Stat codes are INT CHA VIT WIL WEA REP HAP MOR - anything else uppercase is a typo.
stat "…" needs a comparisonBare uppercase names aren’t conditions; write INT >= 50. Bare lowercase names are variable checks.
dangling "&&"Trailing operator with nothing after it.
MessageCause / fix
cannot parse effect "…"Each term must be STAT+N or STAT-N. Variables don’t go in *effects: - use *set:.
stat "…" appears twiceMerge the deltas into one term.
cannot parse assignment "…"*set: takes name, name = true/false, name = <int>, name = random(<sides>), or name += <int> / name += random(<sides>).
"…" is declared as a flag, and counting … is not something a flag does+= and -= count, so they need an integer or random(N). Plain = is what sets true/false.
"*unset:" takes only variable namesNo = in *unset: - it erases; to assign, use *set:.
invalid variable name "…"Variables are snake_case. Uppercase means you wanted *effects:.
"age" is reservedage and played can’t be variables.
MessageCause / fix
"…" is not declared in this fragmentEvery variable you touch goes in a *use block (shared state somebody else made) or a *declare block (state you’re introducing). Usually a typo: without declarations a misspelled name reads as 0 and its branch silently never fires, which is why this is an error.
no shared variable by that name exists - did you mean "…"?The name in your *use block isn’t registered. Take the suggestion, or ask in your submission thread if you’re introducing it.
"…" is declared as a flag, and … is not something a flag doesTypes are enforced. += and >= are for a count; = <member> is for an enum; true/false is a flag.
"…" is not a member of enum "…"Set it to one of the members you listed, or add the member to the declaration.
only a local or an innate may declare a valueShared state starts unset, because no fragment can know what ran before it. Mark it local, make it innate if the character is born with it, or drop the =.
innate "…" needs the value it is born withAn innate is settled at birth, so the declaration has to say what it settles on: = random(6) for a count, = chance(10) for a flag, or a constant.
"…" is declared both local and innateThe two disagree about who the fact belongs to. An innate nobody else can read is a local with a default, which already works.
"random(6)" rolls a number, so it belongs to a countOne roll per type: random(N) spreads a count over 1..N, chance(N) makes a flag true in N lives per hundred.
chance(100) must be between 1 and 99For always or never, say true or false and read as if you meant it.
innate "…" is random(6) here and random(20) in …Two fragments declaring the same trait have to declare the same one. A life cannot be born on two different dice.
unterminated "*declare" blockAdd the *enddeclare (a *use block wants *enduse). The line number is where the block opened.
a fragment has one "*use" blockMerge them; same for *declare.
"…" is named twice in this fragment's headerOne name means one thing per fragment, so a variable and a world entity cannot share one. Rename whichever reads worse.
MessageCause / fix

| "…" is not an attribute of a character | Attributes are name: and note:. An unrecognised one would be written and never read, so it is refused rather than dropped. | | attributes are "key: "value"" pairs separated by commas | Every attribute value is quoted: { name: "Kevin", note: "loyal, reckless" }. | | no character or anything else by that name - did you mean "…"? | The name in your *use block is not in content/world.json. Take the suggestion, or say in your submission thread that you are introducing them. | | "…" is a character here, but content/world.json has it as a place | Two different things are sharing an id. The registry is what every other fragment reads. |

MessageCause / fix
choice depth jumps from N to MNest one level at a time: > then >> then >>>.
"*requires:" must directly follow its choice line*requires: lines go immediately under the > line, before any prose or effects.
… can offer a player nothing but locked doorsEvery choice the scene presents is gated, so a character who meets none of the gates sees a wall and a Continue. Leave one ungated, or add *exhausted: to say where they go instead. A choice that only exists inside an *if does not count, since the arm can fire alone.
fragment "…" never offers the player a choiceThe entry has no choices and nothing it can reach with -> has any either. The choices do not have to be in the entry: an entry may hand over to a subscene that holds them. Somewhere down the line, the player has to get to pick something.
MessageCause / fix
unclosed "*if" block starting at line NAdd the missing *endif.
"*endif" has no "*if" block to closeA closer with nothing above it to close - the opener is missing or was deleted. Same for *endrandom, *enduse, *enddeclare, and for a stray *else.
"*end" is not a closer*end closed every block once and closes none now. Each block names what it ends: *endif, *endrandom, *enduse, *enddeclare.
"*endif" is the closer for "*if" - this block closes with "*endrandom"The right closer, on the wrong block. Reported at the closer, which is the line that is wrong.
"*random" block with a single "*chance" armRandomness needs at least two arms.
unclosed "*random" block starting at line NAdd the missing *endrandom.
"*chance" arm outside of a "*random" blockArms only live between *random and *endrandom.
cannot parse "*chance …" - expected "*chance <weight>"Weights are positive whole numbers.
nav target "…" does not match any subscene-> targets must be @ scenes in the same fragment (or death).
conditional navigation is gone-> [cond] target was shorthand for an *if block holding a nav. Write the block; the message shows it.
unreachable navA -> above it always wins, so this one can never run; reorder or delete. An error rather than a warning like other dead content, because a jump that never fires is invisible in play: the player simply ends up somewhere else.
"*exhausted:" target "…" does not match any subsceneLike a -> target, it must be an @ scene in the same fragment (or death).
"*exhausted:" needs a target subscene / target "…" contains whitespaceOne snake_case id, e.g. *exhausted: leaving.
a scene can have only one "*exhausted:" lineMerge them; a scene has a single way out when it runs dry.
"*exhausted:" belongs to the scene itselfIt was inside an *if/*random block. Move it to the scene’s top level.
MessageCause / fix
unclosed {if}Every {if …} needs its {endif}.
"{endif}" without an opening "{if}"Stray closer - check nesting.
unknown inline token "{…}"A token must be a conditional, a pronoun token, or a value read (a stat like {WEA}, {age}, or a snake_case variable). Mixed case, spaces, and hyphens are none of those.
"{@…}" is goneThere is no token for a name. Write it: a character called Kevin is called Kevin. The *use or *declare line is what records that your fragment features them.
"…" needs a "description:"A *declared world entity has to say who or what it is. That sentence is what goes into the registry and what the next writer reads.
"…" already exists - … introduced it*declare is for state or people nobody has yet. Move it to a *use block.
"reserved" on … takes true or false without quotesIt is a fact, not a phrase, and "false" in quotes would read as true.
a transient looks like "*tmp count i = 0"A *tmp always says what it starts as, because it starts fresh on every run.
unterminated block commentA \* fence was opened and never closed. The line number is where it opened.
closes a block comment that was never openedA *\ with no \* above it.
block comment fences go on their own lines\* … *\ written on a single line. Put each fence on its own line.
unknown stat "{…}" in proseAn all-caps value token must name a real stat: INT CHA VIT WIL WEA REP HAP MOR.

*if, *set, *beat, @@, variant:, followup:, and - sub-choices are from the pre-2026 format. The error message names the v2 replacement; the full mapping is in the repo’s docs/frag-format.md under “Migrating from v1”.

WarningMeaning
has no tagsSelection diversity uses the first tag; untagged fragments opt out.
remove the "author:" lineYou do not sign your own fragments. Authorship is written when a submission is accepted, from the handle you registered under, so delete the line. If you copied it from an old example, that example is out of date.
choice "…" has an empty bodySelecting it does nothing visible and ends the fragment. Write the outcome, send it somewhere with ->, or delete the choice.
effect … is a large swingFires above 16. Allowed, but should be a deliberate, rare event. The style guide advises staying within 10, so most of the range between is quiet on purpose rather than endorsed.
subscene "@ …" is never navigated toDead scene - delete it or wire it up.
… puts … after a "->"A -> ends the body it stands in, so nothing below it in the same body ever runs. Most often a choice written under a scene’s ->, which then never appears. Move it above the ->, or into the subscene the -> points at. A -> inside an *if arm does not strand what follows the block, and is not warned about.
… can be revisited, and "…" fires again on every visitStat effects at the top level of a hub re-fire on every loop. Gate them behind *if visits = 1, or move them into a choice.
sticky choice "…" … grants stats every time it is takenA >* choice in a loop is farmable. Drop the *, or gate the reward.
prose reads variable "{…}" but nothing in this file sets itUsually a typo, since an unknown snake_case token prints 0 rather than failing. Harmless when another file sets the variable.