Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cursor/rules/learnings-index.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ propose a retirement, a consolidation, or a glob-scoped sub-index split.
step that must run after one (installed stamp, hygiene hooks): project
`run:` commands exec-replace the dev process, so nothing after
`cmd.execute` in Runner#run runs for them — post-command work needs a
builtin or spawn-and-wait (dev#85).
builtin or CommandRunner wait mode (dev#85).
→ .cursor/skills/learnings/command-runner-exec/

## toolchain
Expand Down
38 changes: 20 additions & 18 deletions .cursor/skills/learnings/command-runner-exec/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,33 @@ description: >-

# CommandRunner exec ends the dev process

`Dev::CommandRunner` runs project `run:` commands with `Kernel.exec` — the
dev process is replaced, so nothing after `cmd.execute` in
`Dev::CommandRunner` runs project `run:` commands with `Kernel.exec` by
default — the dev process is replaced, so nothing after `cmd.execute` in
`Dev::Runner#run` executes for a yaml-declared command, nor for an
`OverriddenCommand` whose body is one. Post-execute steps only ever run
for fully in-process builtins — which makes this an input to command
placement: a builtin can carry post-steps, a `run:` command cannot.
`OverriddenCommand` whose body is one. Must-happen work cannot sit after
a maybe-exec point: it lives in a fully in-process builtin, or the
command must run in CommandRunner's wait mode.

Wrong — a follow-up step after execute, expecting it for every command:

cmd.execute(args:, context:)
stamp_installed(cmd_name, context.project_root) # skipped on exec

Also wrong: hoisting the step before execute, when it records an outcome
Also wrong: hoisting the step before execute when it records an outcome
(the stamp means "provisioning *succeeded*" — stamping first marks a
failed `dev up` as installed). Success-contingent work needs the command
to run in-process: spawn-and-wait with the exit status propagated, not
exec-replace — that fix is dev#85. Only outcome-independent work may
move before the exec point.
failed `dev up` as installed). Only outcome-independent work may move
before the exec point.

Observed symptom: in a repo whose dev.yml defines `up:`, `dev up` exec's
into the project's up command and never reaches `stamp_installed`, so the
staleness gate keeps reporting "never installed" — fatal in a CI=true
shell. `dev install-deps` stays in-process and stamps. Check this before
suspecting the staleness digests themselves.
Right (dev#85): Runner sets `wait: true` on ExecutionContext for
`STAMPING_COMMANDS` (`up`, `install-deps`); CommandRunner then runs the
child spawn-and-wait (`Kernel.system`) instead of exec-replace, raising
`CommandFailedError` with the child's exit status on failure, which
Runner turns into `Kernel.exit` — stamp only on success, exit code
preserved. Generic `run:` commands keep the exec tail-call (TTY/signal
passthrough, no double process tree). Diagnostic signature of a missing
wait: an exec-style provisioning command "succeeds" but the staleness
gate keeps reporting "never installed" — fatal in a CI=true shell.

learned-from: dev#73 build pass (dev up never stamped; install-deps did);
the sequencing bug itself is dev#85
date: 2026-08-02
learned-from: dev#73 build pass (dev up never stamped; install-deps
did); fixed by CommandRunner wait mode in dev#85
date: 2026-08-03
Loading