# Install UX Hardening — IUH-M01 Session Notes ## Session: 2026-04-05 (agent-ad6b6696) ### Plan **Manifest schema decision:** - Version 1 JSON at `~/.config/mosaic/.install-manifest.json` (mode 0600) - Written by `tools/install.sh` after successful install - Fields: version, installedAt, cliVersion, frameworkVersion, mutations{directories, npmGlobalPackages, npmrcLines, shellProfileEdits, runtimeAssetCopies} - Uninstall reads it; if missing → heuristic mode (warn user) **File list:** - NEW: `packages/mosaic/src/runtime/install-manifest.ts` — read/write helpers + types - NEW: `packages/mosaic/src/runtime/install-manifest.spec.ts` — unit tests - NEW: `packages/mosaic/src/commands/uninstall.ts` — command implementation - NEW: `packages/mosaic/src/commands/uninstall.spec.ts` — unit tests - MOD: `packages/mosaic/src/cli.ts` — register `uninstall` command - MOD: `tools/install.sh` — write manifest on success + add `--uninstall` path **Runtime asset list (from mosaic-link-runtime-assets / framework/install.sh):** - `~/.claude/CLAUDE.md` (source: `$MOSAIC_HOME/runtime/claude/CLAUDE.md`) - `~/.claude/settings.json` (source: `$MOSAIC_HOME/runtime/claude/settings.json`) - `~/.claude/hooks-config.json` (source: `$MOSAIC_HOME/runtime/claude/hooks-config.json`) - `~/.claude/context7-integration.md` (source: `$MOSAIC_HOME/runtime/claude/context7-integration.md`) - `~/.config/opencode/AGENTS.md` (source: `$MOSAIC_HOME/runtime/opencode/AGENTS.md`) - `~/.codex/instructions.md` (source: `$MOSAIC_HOME/runtime/codex/instructions.md`) **Reversal logic:** 1. If `.mosaic-bak-` exists for a file → restore it 2. Else if managed copy exists → remove it 3. Never touch files not in the known list **npmrc reversal:** - Only remove line `@mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/` - If manifest has the line, use that as authoritative; else check heuristically **PATH reversal:** - Check install.sh: it does NOT add PATH entries to shell profiles (framework/install.sh migration removes old `$MOSAIC_HOME/bin` PATH entries in v0/v1→v2 migration, but new install does NOT add PATH) - ASSUMPTION: No PATH edits in current install (v0.0.24+). Shell profiles not modified by current install. - The `$PREFIX/bin` is mentioned in a warning but NOT added to shell profiles by install.sh. - shellProfileEdits array will be empty for new installs; heuristic mode also skips it. **Test strategy:** - Unit test manifest read/write with temp dir mocking - Unit test command registration - Unit test dry-run flag (no actual fs mutations) - Unit test --keep-data skips protected paths - Unit test heuristic mode warning **Implementation order:** 1. install-manifest.ts helpers 2. install-manifest.spec.ts tests 3. uninstall.ts command 4. uninstall.spec.ts tests 5. cli.ts registration 6. tools/install.sh manifest writing + --uninstall path ASSUMPTION: No PATH modifications in current install.sh (v0.0.24). Framework v0/v1→v2 migration cleaned old PATH entries but current install does not add new ones. ASSUMPTION: `--uninstall` in install.sh handles framework + cli + npmrc only; gateway teardown deferred to `mosaic gateway uninstall`. ASSUMPTION: Pi settings.json edits (skills paths) added by framework/install.sh are NOT reversed in this iteration — too risky to touch user Pi config without manifest evidence. Noted as follow-up. --- ## Session 2 — 2026-04-05 (orchestrator resume) ### IUH-M01 completion summary - **PR:** #429 merged as `25cada77` - **CI:** green (Woodpecker) - **Issue:** #425 closed - **Files:** +1205 lines across 4 new + 2 modified + 1 docs - **Tests:** 14 new, 170 total passing ### Follow-ups captured from worker report 1. **Pi settings.json reversal deferred** — worker flagged as too risky without manifest evidence. Future IUH task should add manifest entries for Pi settings mutations. Not blocking M02/M03. 2. **Pre-existing `cli-smoke.spec.ts` failure** — `@mosaicstack/brain` package entry resolution fails in Vitest. Unrelated to IUH-M01. Worth a separate issue later. 3. **`pr-create.sh` wrapper bug with multiline bodies** — wrapper evals body args as shell when they contain newlines/paths. Worker fell back to Gitea REST API. Same class of bug I hit earlier with `issue-create.sh`. Worth a tooling-team issue to fix both wrappers. ### Mission doc sync cli-unification docs that were archived before the M01 subagent ran did not travel into the M01 PR (they were local, stashed before pull). Re-applying now: - `docs/archive/missions/cli-unification-20260404/` (the old manifest + tasks) - `docs/MISSION-MANIFEST.md` (new install-ux-hardening content) - `docs/TASKS.md` (new install-ux-hardening content) Committing as `docs: scaffold install-ux-hardening mission + archive cli-unification`. ### Next action Delegate IUH-M02 to a sonnet subagent in an isolated worktree. --- ## Session 3: 2026-04-05 (agent-a6ff34a5) — IUH-M02 Wizard Remediation ### Plan **AC-3: Password masking + confirmation** - New `packages/mosaic/src/prompter/masked-prompt.ts` — raw-mode stdin reader that suppresses echo, handles backspace/Ctrl+C/Enter. - `bootstrapFirstUser` in `packages/mosaic/src/commands/gateway/install.ts`: replace `rl.question('Admin password...')` with `promptMaskedPassword()`, require confirm pass, keep min-8 validation. - Headless path: when `MOSAIC_ASSUME_YES=1` or `!process.stdin.isTTY`, read `MOSAIC_ADMIN_PASSWORD` env var directly. **AC-4a: Hooks preview stage** - New `packages/mosaic/src/stages/hooks-preview.ts` — reads `hooks-config.json` from `state.sourceDir` or `state.mosaicHome`, displays each top-level hook category with name/trigger/command preview, prompts "Install these hooks? [Y/n]", stores result in `state.hooks`. - `packages/mosaic/src/types.ts` — add `hooks?: { accepted: boolean; acceptedAt?: string }` to `WizardState`. - `packages/mosaic/src/wizard.ts` — insert `hooksPreviewStage` between `runtimeSetupStage` and `skillsSelectStage`; skip if no claude runtime detected. **AC-4b: `mosaic config hooks` subcommands** - Add `hooks` subcommand group to `packages/mosaic/src/commands/config.ts`: - `list`: reads `~/.claude/hooks-config.json`, shows hook names and enabled/disabled status - `disable `: prefixes matching hook key with `_disabled_` in the JSON - `enable `: removes `_disabled_` prefix if present **AC-5: Headless install path** - `runConfigWizard`: detect headless mode (`MOSAIC_ASSUME_YES=1` or `!process.stdin.isTTY`), read env vars with defaults, validate required vars, skip prompts entirely. - `bootstrapFirstUser`: detect headless mode, read `MOSAIC_ADMIN_NAME/EMAIL/PASSWORD`, validate, proceed without prompts. - Document env vars in `packages/mosaic/README.md` (create if absent). ### File list NEW: - `packages/mosaic/src/prompter/masked-prompt.ts` - `packages/mosaic/src/prompter/masked-prompt.spec.ts` - `packages/mosaic/src/stages/hooks-preview.ts` - `packages/mosaic/src/stages/hooks-preview.spec.ts` MODIFIED: - `packages/mosaic/src/types.ts` — extend WizardState - `packages/mosaic/src/wizard.ts` — wire hooksPreviewStage - `packages/mosaic/src/commands/gateway/install.ts` — masked password + headless path - `packages/mosaic/src/commands/config.ts` — add hooks subcommands - `packages/mosaic/src/commands/config.spec.ts` — extend tests - `packages/mosaic/README.md` — document env vars ### Assumptions ASSUMPTION: `hooks-config.json` location is `/framework/runtime/claude/hooks-config.json` during wizard (sourceDir is package root). Fall back to `/runtime/claude/hooks-config.json` for installed config. ASSUMPTION: The `hooks` subcommands under `config` operate on `~/.claude/hooks-config.json` (the installed copy), not the package source. ASSUMPTION: For the hooks preview stage, the "name" field displayed per hook entry is the top-level event key (e.g. "PostToolUse") plus the matcher from nested hooks array. This is the most user-readable representation given the hooks-config.json structure. ASSUMPTION: `config hooks list/enable/disable` use `CLAUDE_HOME` env or `~/.claude` as the target directory for hooks files. ASSUMPTION: The headless TTY detection (`!process.stdin.isTTY`) is sufficient; `MOSAIC_ASSUME_YES=1` is an explicit override for cases where stdin is a TTY but the user still wants non-interactive (e.g., scripted installs with piped terminal).