Skip to main content

Getting started

Strategies are loaded by whichever host you use — the desktop app's trainer, or the console. Both read the same directory and run the same strategy runner; only how you point them at it, and how you watch it work, differs.

1. Use the strategies directory

Both hosts read strategies from ~/.kebab/strategies. It is not created for you on a fresh install:

mkdir -p ~/.kebab/strategies

Any .yaml or .yml file in it gets loaded. Files with other extensions are ignored, and subdirectories are not recursed into.

The filename does not name the strategy. The name: field inside the file does. Two files whose name: matches are one strategy, and the one that loads later wins — see File format → name.

2. Write a file

~/.kebab/strategies/storm-hitter.yaml:

$schema: https://scm.kebab.sh/strat/config.json
name: storm-hitter
school: storm

targets:
default_enemy: boss

fallback: pass

phases:
setup:
priority: 50
when:
no_blade: storm
actions:
- blade: { school: storm }

execute:
priority: 10
actions:
- cast: { type: damage, school: storm }
- wand_hit: { target: first }

3. Load it: the desktop app

The trainer reads ~/.kebab/strategies on startup — there is no flag or setting for it. Once loaded, storm-hitter becomes selectable by name wherever an automation step takes a strategy, under the Trainer's Build tab. A team file registers one entry per member, named <strategy>/<member> — see Teams.

Loading the directory is all-or-nothing: one file that fails to parse fails the whole directory. Unlike the console, this does not stop the app — it logs a warning and continues running with no strategies available until the bad file is fixed.

4. Load it: the console

The console loads the same folder. It picks up ~/.kebab/strategies on its own when that folder exists, so no flag is needed:

console --sudo

Pass --strategy to load a different folder instead:

console --sudo --strategy ~/work/strategies

--sudo is only needed on Linux (input-device and Wine ptrace access); drop it on Windows or macOS.

hook
strategies
strategies validate
strategy explain storm-hitter
combat dry-run storm-hitter
enable combat storm-hitter
status

These are console commands — the desktop app does not have a command line, and does the same jobs through the trainer's own controls instead.

CommandDoes
strategiesList the built-ins plus every loaded YAML strategy, with the directory they came from
strategies reloadRe-read the strategy directory
strategies validateRe-parse every file independently and print a per-file pass/fail report, with the file path and the name: it resolved to
strategy explain <name>Print the phase and action tree the loader built
combat dry-run <name>The same tree, plus a target preview naming the hooked clients (and, for a team, the member roles) the strategy would bind to
enable combat <name>Start the runner
disable combatStop it
combat statusWhether the runner is on, and with which strategy
combat restartStop and restart the current strategy — the quickest way to pick up an edit after strategies reload

Run strategies validate after every edit. Loading the directory is all-or-nothing the same way it is for the desktop app, but at console startup that failure is fatal: the console refuses to start and prints the error. validate is the only command that tells you which specific file is at fault, and because it prints each file's path next to the name: it resolved to, it is also how you spot two files claiming the same name.

combat dry-run is the one to reach for before a fight. It answers "will this strategy actually attach to anything?" without engaging combat.

Editor support

There is a JSON schema published at https://scm.kebab.sh/strat/config.json. Two ways to attach it:

$schema: https://scm.kebab.sh/strat/config.json
name: storm-hitter

or, if your editor prefers the language-server directive:

# yaml-language-server: $schema=https://scm.kebab.sh/strat/config.json
name: storm-hitter

Both work. The top-level $schema field is explicitly allowed by the loader, so it will not trip the unknown-field check.

The schema catches misspelled fields and wrong types as you type, which is worth having because the loader's error messages are accurate but arrive later.

Which host is running it

Both hosts parse the file with the same loader and evaluate it with the same runner, so priorities, conditions, expressions, action ordering, enchant policy, and fallback behave identically. What differs is everything the runner needs from outside the file.

libwiz consoleDesktop app trainer
Strategy folder~/.kebab/strategies, or --strategy <dir>~/.kebab/strategies only
A file that fails to loadFatal at startup; nothing loadsLogged as a warning; nothing loads, app keeps running
Strategy name lookupCase-insensitiveCase-sensitive
clients: scopingEnforced — each member binds to its listed aliasesIgnored — the strategy runs against whichever client the automation step names
target: p1, default_ally: p2WorksNever resolves — the action is skipped every round
has_blade: { target: p1 } and friendsWorksAlways false (so no_blade: { target: p1 } is always true)
expr: "p1.health < 50%"WorksAlways false
A team file (members:)Selected by the file's name:; all members start together and each binds to its clientsRegistered as name/member; you select and start each member separately
Built-in strategy namesaoe, pass, fleepass, flee, priority, configurable
Default when a plugin asks for combat with no strategyaoepriority
Per-round decision traceenable dbg prints the selected phase, action, hand and target every roundNot wired up
Client aliases are console-only

p1, p2, … are resolved from a table of live client owner IDs that only the console populates. On the desktop app that table is empty, so:

  • an action with target: p1 never resolves and is silently skipped;
  • targets: { default_ally: p2 } skips every ally-targeting action;
  • a condition scoped to an alias is always false, which flips no_blade: { target: p1 } to permanently true and makes the guarded action fire every round.

Any strategy built around aliases — the whole support-buffs-the-hitter pattern — is a console strategy. On the desktop app, express the same idea with self, lowest_health_ally, or the per-member targets defaults, and drive each character from its own member.

A plugin's default strategy is not the same on both hosts

automation:EnsureCombat with no strategy asks the console for aoe and the desktop app for priority. Neither name exists on the other host: aoe is not registered in the app, and priority is not registered in the console. Always name the strategy explicitly.

The mental model

Before anything else, the runner checks whether you are stunned. If you are, it passes the round immediately and no phase is evaluated at all. Otherwise it:

  1. Sorts phases by priority, highest first.
  2. Finds the first phase whose when: is satisfied — a phase with no when: always qualifies.
  3. Walks that phase's actions in order.
  4. For each action, evaluates its own when: guard, then tries to resolve a card.
  5. Casts the first action that resolves.
  6. If nothing in the phase resolves, moves to the next eligible phase.
  7. If no phase produces an action, uses fallback (pass or flee).

Two consequences worth internalising:

Order is everything. Within a phase, put specific actions before general ones. A bare cast: { type: damage } at the top will swallow every round and nothing below it runs.

A phase that matches but resolves nothing is not a dead end. The runner falls through to the next eligible phase. This is what makes a high-priority emergency phase safe — if the heal is not in hand, play continues normally rather than stalling.

Common first mistakes

No fallback. Without one, a round where nothing resolves does nothing at all. Set fallback: pass unless you genuinely want automatic fleeing.

Relying on the priority tiebreak. The loader sorts phases by priority, highest first, and breaks ties alphabetically by phase name — deterministic, but not something to design around. Give every phase an explicit, distinct priority instead.

Targeting nonsense. A heal aimed at boss will not do what you want. Match the target to the spell: boss or lowest_health for damage, lowest_health_ally or self for heals, p1 for buffing a specific teammate.

Assuming clients: is required. Omit it and the strategy applies to every hooked client. Add it only when you deliberately want to scope — and note that the desktop app ignores it entirely.

Reusing a name:. Copying a file and editing only the phases leaves two files claiming the same strategy. One of them silently disappears. Change name: first, every time.

Using a reserved name. pass and flee are built-in strategy names on both hosts, aoe on the console and priority/configurable in the desktop app. A file claiming one of those names is either unreachable or replaces the built-in. Pick something else.

Next