fix(mosaic): stop yolo runtime from leaking runtime name as first user message
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful

`mosaic yolo <runtime>` forwarded `cmd.args` (which Commander populates with
the declared `<runtime>` positional) to the underlying CLI. For the claude
runtime that meant argv included the literal string "claude" as an initial
positional argument, which Claude Code interpreted as the first user message.
As a secondary consequence, the mission-auto-prompt path was also bypassed
because `hasMissionNoArgs` saw a non-empty args list.

Refactor the runtime subcommand wiring into an exported
`registerRuntimeLaunchers(program, handler)` factory with a
`RuntimeLaunchHandler` type so the commander integration is unit-testable
without spawning subprocesses. Slice the declared positional off `cmd.args`
in the yolo action before forwarding.

Adds `packages/mosaic/src/commands/launch.spec.ts` with 11 regression tests
covering all four runtimes (claude, codex, opencode, pi) for both `<runtime>`
and `yolo <runtime>` paths, excess-args forwarding, and invalid-runtime
rejection. The critical assertion blocks the regression: yolo parses must
forward `extraArgs = []`, not `[runtime]`.

Fixes mosaicstack/stack#454

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 11:53:22 -05:00
parent 81c1775a03
commit 1dd4f5996f
3 changed files with 251 additions and 5 deletions

View File

@@ -0,0 +1,108 @@
# Hotfix Scratchpad — `mosaic yolo <runtime>` passes runtime name as initial user message
- **Issue:** mosaicstack/stack#454
- **Branch:** `fix/yolo-runtime-initial-arg`
- **Type:** Out-of-mission hotfix (not part of Install UX v2 mission)
- **Started:** 2026-04-11
## Objective
Stop `mosaic yolo <runtime>` from passing the runtime name (`claude`, `codex`, etc.) as the initial user message to the underlying CLI. Restore the mission-auto-prompt path for yolo launches.
## Root cause (confirmed)
`packages/mosaic/src/commands/launch.ts:779` — the `yolo <runtime>` action handler:
```ts
.action((runtime: string, _opts: unknown, cmd: Command) => {
// ... validate runtime ...
launchRuntime(runtime as RuntimeName, cmd.args, true);
});
```
Commander.js includes declared positional arguments in `cmd.args`. For `mosaic yolo claude`:
- `runtime` (destructured) = `"claude"`
- `cmd.args` = `["claude"]` — the same value
`launchRuntime` treats `["claude"]` as excess positional args, and for the `claude` case that becomes the initial user message. As a secondary consequence, `hasMissionNoArgs` evaluates false, so the mission-auto-prompt path is bypassed too.
## Live reproduction (intercepted claude binary)
```
$ PATH=/tmp/fake-claude-bin:$PATH mosaic yolo claude
[mosaic] Launching Claude Code in YOLO mode...
argv[1]: --dangerously-skip-permissions
argv[2]: --append-system-prompt
argv[3] (len=25601): # ACTIVE MISSION — HARD GATE ...
argv[4]: claude ← the bug
```
Non-yolo variant `mosaic claude` is clean:
```
argv[1]: --append-system-prompt
argv[2]: <prompt>
argv[3]: Active mission detected: MVP. Read the mission state files and report status.
```
## Plan
1. Refactor `launch.ts`: extract `registerRuntimeLaunchers(program, handler)` with an injectable handler so commander wiring is testable without spawning subprocesses. `registerLaunchCommands` delegates to it with `launchRuntime` as the handler.
2. Fix: in the `yolo <runtime>` action, pass `cmd.args.slice(1)` instead of `cmd.args`.
3. Add `packages/mosaic/src/commands/launch.spec.ts`:
- Failing-first reproducer: parse `['node','x','yolo','claude']` and assert handler receives `extraArgs=[]` and `yolo=true`.
- Regression test: parse `['node','x','claude']` asserts handler receives `extraArgs=[]` and `yolo=false`.
- Excess args: parse `['node','x','yolo','claude','--print','hi']` asserts handler receives `extraArgs=['--print','hi']` (with `--print` kept because `allowUnknownOption` is true).
- Excess args non-yolo: parse `['node','x','claude','--print','hi']` asserts `extraArgs=['--print','hi']`.
- Reject unknown runtime under yolo.
4. Run typecheck, lint, format:check, vitest for `@mosaicstack/mosaic`.
5. Independent code review (feature-dev:code-reviewer subagent, sonnet tier).
6. Commit → push → PR via wrappers → merge → CI green → close issue #454.
7. Release decision (`mosaic-v0.0.30`) deferred to Jason after merge.
## Framework compliance sub-findings (out-of-scope; to capture in OpenBrain after)
- `~/.config/mosaic/tools/git/issue-create.sh` uses `eval` on `$BODY`; arbitrary bodies with backticks, `$`, or parens break catastrophically.
- `gitea_issue_create_api` fallback uses `curl -fsS` without `-L`; after the `mosaicstack/mosaic-stack → mosaicstack/stack` rename, the API redirect is not followed and the fallback silently fails.
- Local repo `origin` remote still points at old `mosaic/mosaic-stack.git` slug. Not touched here per git-config safety rule.
- `~/.config/mosaic/TOOLS.md` referenced by the global load order but does not exist on disk.
These will be captured to OpenBrain after the hotfix merges so they don't get lost, and filed as separate tracking items.
## Progress checkpoints
- [x] Branch created (`fix/yolo-runtime-initial-arg`)
- [x] Issue #454 opened
- [x] Scratchpad scaffolded
- [x] Failing test added (red)
- [x] Refactor + fix applied
- [x] Tests green (launch.spec.ts 11/11)
- [x] Baselines green (typecheck, lint, format:check, vitest — pre-existing `uninstall.spec.ts:138` failure on branch main acknowledged, not caused by this change)
- [x] Code review pass (feature-dev:code-reviewer, sonnet — no blockers)
- [ ] Commit + push
- [ ] PR opened
- [ ] CI queue guard cleared
- [ ] PR merged (squash)
- [ ] CI green on main
- [ ] Issue #454 closed
- [ ] Scratchpad final evidence entry
## Tests run
- `pnpm --filter @mosaicstack/mosaic run typecheck` → green
- `pnpm --filter @mosaicstack/mosaic run lint` → green
- `pnpm --filter @mosaicstack/mosaic exec prettier --check "src/**/*.ts"` → green
- `pnpm --filter @mosaicstack/mosaic exec vitest run src/commands/launch.spec.ts` → 11/11 pass
- `pnpm --filter @mosaicstack/mosaic exec vitest run` → 270/271 pass (1 pre-existing `uninstall.spec.ts:138` EACCES failure, confirmed on the branch before this change)
- `pnpm typecheck` (repo) → green
- `pnpm lint` (repo) → green
- `pnpm format:check` (repo) → green (after prettier-writing the scratchpad)
## Risks / blockers
None expected. Refactor is small and the Commander API is stable. Test needs `exitOverride()` to prevent `process.exit` on invalid runtime.
## Final verification evidence
(to be filled at completion)