12 KiB
Install UX v2 — Orchestrator Scratchpad
Session 1 — 2026-04-05 (orchestrator scaffold)
Trigger
Real-run testing of @mosaicstack/mosaic@0.0.25 (fresh install of the release we just shipped from the parent mission install-ux-hardening-20260405) surfaced a critical regression and a cluster of UX failings. User feedback verbatim:
The skill/additional feature installation section of install.sh is unsable The "quick-start" is asking way too many questions. This process should be much faster to get a quick start. The installater should have a main menu that allows for a drill-down install approach. "Plugins" — Install Recommended Plugins / Custom "Providers" — … The gateway port is not prefilling with 14242 for default What is the CORS origin for? Is that the webUI that isn't working yet? Maybe we should ask for the fqdn/hostname instead? There must be a better way to handle this.
Plus the critical bug, reproduced verbatim:
◇ Admin email
│ jason@woltje.com
Admin password (min 8 chars): ****************
Confirm password: ****************
│
▲ Bootstrap failed (400): {"message":["property email should not exist","property password should not exist"],"error":"Bad Request","statusCode":400}
✔ Wizard complete.
✔ Install manifest written: /home/jarvis/.config/mosaic/.install-manifest.json
✔ Done.
Note the ✔ Wizard complete and ✔ Done lines after the 400. That's a second bug — failure didn't propagate in interactive mode.
Diagnosis — orchestrator pre-scope
To avoid handing workers a vague prompt, pre-identified the concrete fix sites:
Bug 1 (critical) — DTO class erasure. apps/gateway/src/admin/bootstrap.controller.ts:16:
import type { BootstrapSetupDto, BootstrapStatusDto, BootstrapResultDto } from './bootstrap.dto.js';
import type erases the class at runtime. @Body() dto: BootstrapSetupDto then has no runtime metatype — design:paramtypes reflects Object. Nest's ValidationPipe with whitelist: true + forbidNonWhitelisted: true receives a plain Object metatype, treats every incoming property as non-whitelisted, and 400s with "property email should not exist", "property password should not exist".
One-character fix: drop the type keyword on the BootstrapSetupDto import. BootstrapStatusDto and BootstrapResultDto are fine as type-only imports because they're used only in return type positions, not as @Body() metatypes.
Must be covered by an integration test that binds through Nest, not a controller unit test that imports the DTO directly — the unit test path would pass even with import type because it constructs the pipe manually. An e2e test with @nestjs/testing + supertest against the real /api/bootstrap/setup endpoint is the right guard.
Bug 2 — interactive silent failure. packages/mosaic/src/wizard.ts:147-150:
if (!bootstrapResult.completed && headlessRun) {
prompter.warn('Admin bootstrap failed in headless mode — aborting wizard.');
process.exit(1);
}
The guard is && headlessRun. In interactive mode, completed: false is silently swallowed and the wizard continues to the success lines. Fix: propagate failure in both modes. Decision for the worker — either throw or process.exit(1) with a clear error.
Bug 3 — port prefill. packages/mosaic/src/stages/gateway-config.ts:77-88:
const raw = await p.text({
message: 'Gateway port',
defaultValue: defaultPort.toString(),
...
});
The stage is passing defaultValue. Either the WizardPrompter.text adapter is dropping it, or the underlying @clack/prompts call expects initialValue (which actually prefills the buffer) vs defaultValue (which is used only if the user submits an empty string). Worker should verify the adapter and likely switch to initialValue semantics so the user sees 14242 in the field.
Bug 4 — Pi SDK copy gap. The "What is Mosaic?" intro text enumerates Claude Code, Codex, and OpenCode but never mentions Pi SDK, which is the actual agent runtime behind those frontends. Purely a copy edit — find the string, add Pi SDK.
Mission shape
Three milestones, three tracks, different tiers:
- IUV-M01 Hotfix (sonnet) — the four bugs above + release
mosaic-v0.0.26. Small, fast, unblocks the 0.0.25 happy path. - IUV-M02 UX polish (sonnet) — CORS origin → FQDN/hostname abstraction; diagnose and rework the skill installer section. Diagnostic-heavy.
- IUV-M03 Provider-first intelligent flow (opus) — the big one: drill-down main menu, Quick Start path that's actually quick, provider-first natural-language intake with agent self-naming (OpenClaw-style). Architectural.
Sequencing: strict. M01 ships first as a hotfix release (mosaic-v0.0.26). M02 is diagnostic-heavy and can share groundwork with M03 but ships separately for clean release notes. M03 is the architectural anchor and lands last as mosaic-v0.0.27.
Open design questions (to be resolved by workers, not pre-decided)
- M01: does
process.exit(1)vsthrowmatter for howtools/install.shsurfaces the error? Worker should check the install.sh call site and pick the behavior that surfaces cleanly. - M03: what LLM call powers the intent intake, and what's the offline fallback? Options: (a) reuse the provider the user is configuring (chicken-and-egg — provider setup hasn't happened yet), (b) a bundled deterministic "advisor" that hard-codes common intents, (c) require a provider key up-front before intake. Design doc (IUV-03-01) must resolve.
- M03: is the "agent self-naming" persistent across all future
mosaicinvocations, or a per-session nickname? Probably persistent — lives in~/.config/mosaic/agent.jsonor similar. Worker to decide + document.
Non-goals for this mission
- No GUI / web UI
- No registry / pipeline migration
- No multi-user / multi-tenant onboarding
- No rework of
mosaic uninstall(stable from parent mission)
Known tooling caveats (carry forward from parent mission)
issue-create.sh/pr-create.shwrappers have anevalbug with multiline bodies — use Gitea REST API fallback withload_credentials gitea-mosaicstackpr-ci-wait.shreportsstate=unknownagainst Woodpecker (combined-status endpoint gap) — usetea prglyphs or poll the commit status endpoint directly- Protected
main, squash-merge only, PR-required - CI queue guard before push/merge:
~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge
Next action
- Create Gitea issues for M01, M02, M03
- Open the mission-scaffold docs PR (same pattern as parent mission's PR #430)
- After merge, delegate IUV-M01 to a sonnet subagent in an isolated worktree with the concrete fix-site pointers above
Session 2 — 2026-04-05 (IUV-M01 delivery + close-out)
Outcome
IUV-M01 shipped. mosaic-v0.0.26 released and registry latest confirmed 0.0.26.
PRs merged
| PR | Title | Merge |
|---|---|---|
| #440 | fix: bootstrap hotfix — DTO erasure, wizard failure, port prefill, copy | 0ae932ab |
| #441 | fix: add vitest.config.ts to eslint allowDefaultProject (#440 build fix) | c08aa6fa |
| #442 | docs: mark IUV-M01 complete — mosaic-v0.0.26 released | 78388437 |
Bugs fixed (all 4 in worker's PR #440)
- DTO class erasure —
apps/gateway/src/admin/bootstrap.controller.ts:16— droppedtypefromimport { BootstrapSetupDto }. Guarded by new e2e testbootstrap.e2e.spec.ts(4 cases) that binds through a real Nest app withValidationPipe { whitelist, forbidNonWhitelisted }. Test suite neededunplugin-swcinapps/gateway/vitest.config.tsto emitdecoratorMetadata(tsx/esbuild can't). - Wizard silent failure —
packages/mosaic/src/wizard.ts— removed the&& headlessRunguard so!bootstrapResult.completednow aborts in both modes. - Port prefill — root cause was clack's
defaultValuevsinitialValuesemantics (defaultValueonly fills on empty submit,initialValueprefills the buffer). Added aninitialValuefield toWizardPrompter.text()interface, threaded through clack and headless prompters, switchedgateway-config.tsport/url prompts to use it. - Pi SDK copy —
packages/mosaic/src/stages/welcome.ts— intro copy now lists Pi SDK.
Mid-delivery hiccup — tsconfig/eslint cross-contamination
Worker's initial approach added vitest.config.ts to apps/gateway/tsconfig.json's include to appease the eslint parser. That broke pnpm --filter @mosaicstack/gateway build with TS6059 (vitest.config.ts outside rootDir: "src"). The publish pipeline on the #440 merge commit failed.
Correct fix (worker's PR #441): leave tsconfig.json clean (include: ["src/**/*"]) and instead add the file to allowDefaultProject in the root eslint.config.mjs. This keeps the tsc program strict while letting eslint resolve a parser project for the standalone config file.
Pattern to remember: when adding root-level .ts config files (vitest, build scripts) to a package with rootDir: "src", the eslint parser project conflict is solved with allowDefaultProject, NEVER by widening tsconfig include. I had independently arrived at the same fix on a branch before the worker shipped #441 — deleted the duplicate.
Residual follow-ups carried forward
- Headless prompter fallback order: worker set
initialValue > defaultValuein the headless path. Correct semantic, but any future headless test that explicitly depends ondefaultValueprecedence will need review. - Vitest + SWC decorator metadata pattern is now the blessed approach for NestJS e2e tests in this monorepo. Any other package that adds NestJS e2e tests should mirror
apps/gateway/vitest.config.ts.
Next action
- Close out orchestrator doc sync (this commit): mark M01 subtasks done in
TASKS.md, update manifest phase to Execution, commit scratchpad session 2, PR to main. - After merge, delegate IUV-M02 (sonnet, isolated worktree). Dependencies: IUV-02-01 (CORS→FQDN) starts unblocked since M01 is released; first real task for the M02 worker is diagnosing the skill installer failure modes (IUV-02-02) against the fresh 0.0.26 install.
Session 3 — 2026-04-05 (IUV-M02 delivery + close-out)
Outcome
IUV-M02 shipped. PR #444 merged (172bacb3), issue #437 closed. 18 new tests (13 CORS derivation, 5 skill sync).
Changes
CORS → FQDN (IUV-02-01):
packages/mosaic/src/stages/gateway-config.ts— replaced raw "CORS origin" text prompt with "Web UI hostname" (default:localhost). Added HTTPS follow-up for remote hosts. PurederiveCorsOrigin(hostname, port, useHttps?)function exported for testability.- Headless:
MOSAIC_HOSTNAMEenv var as friendly alternative;MOSAIC_CORS_ORIGINstill works as full override. packages/mosaic/src/types.ts— addedhostname?: stringtoGatewayState.
Skill installer rework (IUV-02-02 + IUV-02-03):
- Root cause confirmed:
syncSkills()infinalize.tsignoredstate.selectedSkillsentirely. The multiselect UI was a no-op. packages/mosaic/src/stages/finalize.ts—syncSkills()rewritten to acceptselectedSkills[], returns typedSyncSkillsResult, passesMOSAIC_INSTALL_SKILLS(colon-separated) as env var to the bash script.packages/mosaic/framework/tools/_scripts/mosaic-sync-skills— added bash associative array whitelist filter keyed onMOSAIC_INSTALL_SKILLS. When set, only whitelisted skills are linked. Empty/unset = all skills (legacy behavior preserved formosaic syncoutside wizard).- Failure surfaces: silent
catch {}replaced with typed error reporting throughp.warn().
Next action
- Delegate IUV-M03 (opus, isolated worktree) — the architectural milestone: provider-first intelligent flow, drill-down main menu, Quick Start fast path, agent self-naming. This is the biggest piece of the mission.