Skip to content

feat!: engine block and the runner axis — run scripts in the shell you are in - #8

Merged
MY-RV merged 3 commits into
mainfrom
feat/runner-axis
Sep 21, 2026
Merged

MY-RV merged 3 commits into
mainfrom
feat/runner-axis

Conversation

@MY-RV

@MY-RV MY-RV commented Sep 21, 2026

Copy link
Copy Markdown
Owner

Summary

Breaking. 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.

(Reopened: #6 closed itself when its base branch was deleted after #5 merged. Same content, rebased onto main.)

engine: — what godo needs, apart from what the scripts are

version: "0.1"

engine:
  version: ">=0.3.0"      # minimum binary, enforced before anything runs
  dialect: matcher
  runner: bash
  plugins: [...]

scripts:
  test: go test ./...

scripts: is the data; engine: is every dial godo turns while reading and running it. A top-level dialect: keeps working — it shipped in 0.1 and 0.2.

The default runner is the shell you are in

$ godo arr        # "arr=(a b c); echo ${arr[1]}"
a                 # from zsh — zsh indexes arrays from 1
b                 # from bash

Before, both printed b: godo always used sh. 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, name it (# @runner bash).

Selection: GODO_SHELL$SHELL on Unix → the parent process on Windows, else %ComSpec%. Windows reads the parent because the environment cannot answer: PowerShell sets PSModulePath and everything it starts inherits it.

godo -e runners

Lists what this machine has, and how to check which shell you are actually in — no command run inside a shell can report which shell started godo.

Not in this PR

Checklist

  • Behavior change? Update docs/contract.md in the same PR
  • Tests cover the change (happy path + edge)
  • ./godo ci passes locally
  • No drive-by refactors

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
MY-RV merged commit 16775ec into main Sep 21, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant