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.
| Command | Does |
|---|---|
strategies | List the built-ins plus every loaded YAML strategy, with the directory they came from |
strategies reload | Re-read the strategy directory |
strategies validate | Re-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 combat | Stop it |
combat status | Whether the runner is on, and with which strategy |
combat restart | Stop 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 console | Desktop app trainer | |
|---|---|---|
| Strategy folder | ~/.kebab/strategies, or --strategy <dir> | ~/.kebab/strategies only |
| A file that fails to load | Fatal at startup; nothing loads | Logged as a warning; nothing loads, app keeps running |
| Strategy name lookup | Case-insensitive | Case-sensitive |
clients: scoping | Enforced — each member binds to its listed aliases | Ignored — the strategy runs against whichever client the automation step names |
target: p1, default_ally: p2 | Works | Never resolves — the action is skipped every round |
has_blade: { target: p1 } and friends | Works | Always false (so no_blade: { target: p1 } is always true) |
expr: "p1.health < 50%" | Works | Always false |
A team file (members:) | Selected by the file's name:; all members start together and each binds to its clients | Registered as name/member; you select and start each member separately |
| Built-in strategy names | aoe, pass, flee | pass, flee, priority, configurable |
| Default when a plugin asks for combat with no strategy | aoe | priority |
| Per-round decision trace | enable dbg prints the selected phase, action, hand and target every round | Not wired up |
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: p1never 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.
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:
- Sorts phases by
priority, highest first. - Finds the first phase whose
when:is satisfied — a phase with nowhen:always qualifies. - Walks that phase's
actionsin order. - For each action, evaluates its own
when:guard, then tries to resolve a card. - Casts the first action that resolves.
- If nothing in the phase resolves, moves to the next eligible phase.
- If no phase produces an action, uses
fallback(passorflee).
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
- File format — every top-level field
- Phases — how resolution actually works
- Examples — complete annotated strategies