Conversation
Two gaps, and they are the same gap. A catalog could say which script answers a set of tokens, but not how that script's body becomes a process — that was hardcoded. And there was nowhere to say what godo itself needs, so any such setting would have landed beside the scripts it reads. engine: is that place. scripts: is the catalog, the data; engine: is every dial godo turns while reading and running it — the binary it expects, the dialect, the runner, the plugins. dialect moves there; a top-level dialect: keeps working, because it shipped in 0.1 and 0.2 and there is no reason to break a file over where a key sits. A Runner carries per-invocation state (working directory, stdio), so the registry takes the name at Register rather than reading it off the type, and an unset runner resolves to the Runner injected into the Engine — which keeps NewEngine(cat, runner) working with nothing registered, and lets the CLI decide what the default is. A resolver hook answers for names registered nowhere, which is how a catalog names a shell without godo keeping a list of every shell anyone might have. Unknown runner names fail when the plan is built rather than at load: a dialect must resolve before a script can be matched at all, while a runner is only needed to execute, and the registry that answers for it belongs to the Engine. Both --preview and a plain run fail closed. Each plan step records its own runner, so a dep declaring @runner keeps it instead of inheriting the caller's. ArgsAwareRunner moves one more question to the runner. Reading the body for ${godo:args…} is a fact about shell templates: a plugin's body is a program, has no such placeholder, and would have every extra token rejected. A runner that does not implement it keeps the old policy, message included. engine.plugins is parsed and validated but loaded by nothing. Declaring the shape now is what makes the failure legible — a script asking for a runner a plugin provides names that plugin instead of reading as a typo. sha256 is required from the start: a plugin is third-party code that runs when someone types 'godo test', and a digest is the only thing that says it is the code that was reviewed. config is optional and entirely the plugin's; godo carries it across without reading it.
godo always used sh -c / cmd /C, so a zsh, fish or PowerShell user ran their
catalog under a shell they did not pick — and the divergence was silent rather
than an error: 'arr=(a b c); echo ${arr[1]}' prints a in zsh and b in sh,
because zsh indexes arrays from 1. The shell someone uses is their own
business; godo proxying to a different one was a choice that was not godo's to
make.
The default is now the caller's own shell, and a catalog can name one outright
(# @runner bash) when it wants the same shell for everyone. godo does not
manage those shells: it resolves the name on PATH and hands the line over. The
name is logical, never a path — cmd, not cmd.exe; pwsh, not ps1 — so the same
godo.yaml reads the same everywhere. Names are checked against a list of known
shells, because '# @runner git' would otherwise become 'git -c <line>' and fail
in a way nobody could read.
Consequence, and it is deliberate: a catalog is read by the shell of whoever
runs it, so zsh syntax behaves differently for a teammate on bash. godo is a
proxy and promises neither cross-OS nor cross-shell.
Windows reads the parent process rather than the environment. PowerShell sets
PSModulePath and everything it starts inherits it, so a cmd.exe opened from
PowerShell would look like PowerShell — godo would have run PowerShell syntax
under cmd, silently, which is the exact failure this removes. A parent that is
not a shell falls through to %ComSpec%. It cross-compiles and vets for
windows/amd64 but is unverified on a real Windows host, which is why
GODO_SHELL wins over all of it.
engine.version is enforced here too: a catalog needing a newer binary says so
once and clearly, instead of failing later in whatever way the missing feature
breaks. And godo -e runners prints what this machine actually has, what the
catalog declares from plugins, and — since no command run inside a shell can
report which shell started godo — how to check that in your own terminal.
The old sh/cmd runner is removed rather than kept as a third name: it sat
between 'the shell I am in' and 'this exact shell', and '# @runner sh' says it
better through the same path as every other shell.
The contract gains engine: and the runner axis. The guide gains a runners page in the shape the dialect pages have, with the zsh-vs-bash array example as the thing that makes the default concrete. The design note is premise-first, because the premise is what kept getting lost: godo is a proxy, it does not promise cross-OS, and cross-OS belongs to a plugin rather than to the core. It records what was deliberately not done and why, so the next person proposing an interpreter in the core finds the argument already written down. The roadmap gains the v0.3 row it was missing, marked in progress and held until plugin loading works, since engine: without a loader is half a promise. The repo's own godo.yaml names no runner on purpose, with a comment saying why: its scripts are plain commands, so the caller's shell makes no difference, and the gate dogfoods the new default instead of pinning around it.
MY-RV
force-pushed
the
feat/runner-axis
branch
from
September 20, 2026 21:44
854bcdb to
6e5e342
Compare
4 tasks
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Breaking. Two things, and they are the same thing: a catalog could say which script answers your tokens, but not how its body becomes a process — that was hardcoded to
sh -c/cmd /C. And there was nowhere to say what godo itself needs.Stacked on #5. Review that one first.
engine:— what godo needs, apart from what the scripts arescripts:is the data;engine:is every dial godo turns while reading and running it. A top-leveldialect:keeps working — it shipped in 0.1 and 0.2.The default runner is the shell you are in
Before, both printed
b: godo always usedsh. The shell someone uses is their own business.Consequence, deliberate: a catalog is read by the shell of whoever runs it, so zsh syntax behaves differently for a teammate on bash. godo is a proxy and promises neither cross-OS nor cross-shell. To pin one shell for everyone, name it (
# @runner bash).Selection:
GODO_SHELL→$SHELLon Unix → the parent process on Windows, else%ComSpec%. Windows reads the parent because the environment cannot answer: PowerShell setsPSModulePathand everything it starts inherits it, so acmd.exeopened from PowerShell would look like PowerShell.godo -e runnersPlugins: declared, not loaded
engine.pluginsparses and validates; nothing reads the artifact yet. Declaring the shape now is what makes the failure legible:sha256is required from the start — a plugin is third-party code that runs when someone typesgodo test.configis optional and entirely the plugin's; godo carries it across without reading it.Not in this PR
docs/roadmap.mdmarks v0.3 in progress and held until it works, sinceengine:without a loader is half a promise.windows/amd64but is unverified on a real Windows host.GODO_SHELLoverrides it. Called out in the roadmap and the contract.Checklist
./godo cipasses locally