diff --git a/.cursor/rules/learnings-index.mdc b/.cursor/rules/learnings-index.mdc index bef5c53..7d30d34 100644 --- a/.cursor/rules/learnings-index.mdc +++ b/.cursor/rules/learnings-index.mdc @@ -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 diff --git a/.cursor/skills/learnings/command-runner-exec/SKILL.md b/.cursor/skills/learnings/command-runner-exec/SKILL.md index 68bc724..1c04db6 100644 --- a/.cursor/skills/learnings/command-runner-exec/SKILL.md +++ b/.cursor/skills/learnings/command-runner-exec/SKILL.md @@ -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