# Packaging & Hygiene — Design Spec

Audience: AI coding agents (`run-plan-codex` implementer + reviewer). Optimize for execution, not prose.

- **Slug:** `packaging-hygiene`
- **Date:** 2026-07-02
- **Executor:** `node ~/.claude/workflows/run-plan-codex.js --slug packaging-hygiene`
- **Source audit:** `docs/architecture-gap-audit-2026-07-02.md` — Critical Product Gaps #6, #11–14; Repo Hygiene #34–40.
- **Scope decision:** full set (packaging/install module + remaining critical gaps + hygiene). User confirmed 2026-07-02.

## Purpose

Make host integration reproducible (install/uninstall module, portable packaging, executable wrappers) and stop repo-wide analyzers from tripping on stale worktree/tmp noise. Close the two remaining tool-parity gaps (`CLAUDE_CONFIG_DIR` probe, Claude notify-send fallback).

## Preconditions (HARD — must hold before `run-plan-codex` starts)

1. **`plan/arch-deepening` merged into base.** This plan's `command_router.py`/`CodexAdapter`/`ClaudeAdapter` seam (W5) and tool-qualified `AccountRef` (W3) are load-bearing for T3/T4 below — do not start on a pre-deepening tree.
2. **Quick-fix trio (audit #4/#7/#9) merged into base.** Avoids rebasing packaging tasks over a second in-flight change to `indicator.py`/`account_registry.py`.
3. **This branch landed on local `main` via the repo's existing land pipeline** (`land/<slug>-main` branch + `.worktrees/main-land`, per `finishing-a-development-branch`) — verified 2026-07-02: local `main` (5be9e33) is `origin/main` + 1 commit, not diverged; no reconciliation decision exists. `base_branch` = local `main` post-land.
4. **`meta.base_branch` stamped with fail-closed sentinel `LAND-SET-BASE-BRANCH`** until 1–3 hold, then replaced with the real merged ref. A forgotten edit must not silently target the wrong tree.
5. **`tmp/` and worktree scratch cleared or gitignored (T1 below) before any other task runs** — stale `test_*.py` copies under `tmp/wt-*` collide on import with real `tests/` files and corrupt bare `pytest` collection repo-wide. This is gate0's own trustworthiness; must land first.

## Methodology

Contract-first, behavior-preserving except the explicit fixes named per task. No implementation bodies below — the implementer derives them from the repo.

---

## Modules & seams

### W1 — Verification & hygiene foundation (audit #34, #38)

**T1 · Gitignore worktree/tmp scratch**
- Files: `.gitignore`.
- Contract: `.gitignore` excludes `.worktrees/`, `.wt-*-int/`, `tmp/` (or the specific `tmp/wt-*`/`tmp/cx-implement-logs/` pattern in use) in addition to existing `.claude/`, `__pycache__/`, `.pytest_cache/`.
- Verify: `git status --porcelain` shows none of the above as untracked after a fresh worktree run leaves scratch behind.

**T2 · Lint/type gate**
- Files: `pyproject.toml` (new, ruff config) or `ruff.toml` (new), CI/pre-commit hook if one exists, `mypy.ini` (new) — pick ruff+mypy (industry standard for this stack; no framework already in use to match).
- Contract: `ruff check .` and `mypy .` both runnable, both pass (or documented, justified per-line `# noqa`/`# type: ignore` — never blanket-ignored).
- Test surface: none (tooling); document the exact invocation in `CONTEXT.md` or this spec's follow-up so `run-plan-codex` gate0 can adopt it later.

### W2 — Installable command wrappers (audit #11, #37, #39)

**T1 · Install/uninstall module**
- Files: `install.py` (new, or `installer/__init__.py` if package split from W-hygiene below already happened), `tests/test_install.py` (new).
- Responsibility: single module owns host integration — symlink/copy `cdx`/`cld` wrapper scripts onto `PATH` (e.g. `~/.local/bin`), install desktop entry + icon (see W3), and reverse all of it on uninstall.
- Seam:
  ```
  install(dest_bin: Path = ~/.local/bin, dest_apps: Path = ~/.local/share/applications, dest_icons: Path = ~/.local/share/icons/hicolor/scalable/apps) -> InstallReport
  uninstall(same dests) -> None
  ```
- Contract: idempotent — running `install()` twice does not duplicate entries or error; `uninstall()` on a clean host is a no-op, not an error.
- Test surface: `tests/test_install.py` uses tmp dirs for dest paths, asserts wrapper/desktop/icon files land and reverse cleanly.

**T2 · Executable CLI wrappers**
- Files: `cdx.py`, `cld.py`, `systray_codex_switcher.py` (shebang + mode only — no logic change).
- Contract: `#!/usr/bin/env python3` shebang present; file mode `755`. `install.py` (T1) symlinks these directly rather than generating separate wrapper scripts, unless a name-without-`.py` requirement forces a thin wrapper — prefer symlink, it's the simpler seam.
- Test surface: `tests/test_install.py` (T1) asserts the installed entry is executable and `--help`/no-arg invocation doesn't crash on missing display (headless-safe smoke check).

**T3 · Runtime smoke test for wrappers**
- Files: `tests/test_cli_smoke.py` (new).
- Responsibility: audit #39 — unit tests monkeypatch `exec`/subprocess and never catch real wrapper breakage.
- Contract: at least one test invokes the installed (or symlinked-in-tmp) `cdx`/`cld` entry point as a subprocess (not `main()` call) and asserts nonzero-but-graceful exit + stderr message on expected no-account-configured state — proves the wrapper is actually executable end to end, no mocking the OS boundary.

### W3 — Portable packaging (audit #12, #13)

**T1 · Portable autostart entry**
- Files: `packaging/codex-account-switcher.desktop`, `install.py` (T1 of W2) or a small `packaging.py` (new) — whichever already exists after W2; do not create a second install module.
- Contract: `Exec=` path is rewritten at install time to the actual invocation path (`install.py` template-substitutes, not a hardcoded `/home/user/...` string committed to the repo). The `.desktop` file in the repo carries a placeholder or is generated, never a machine-local absolute path.
- Test surface: extend `tests/test_packaging_desktop_entry.py` — assert the installed `.desktop` file's `Exec=` matches the actual install destination, not a literal from the source file.

**T2 · Icon install automation**
- Files: `install.py` (T1 of W2), `icons/codex-account-switcher.svg` (source, unchanged).
- Contract: `install()` copies the SVG to `~/.local/share/icons/hicolor/scalable/apps/` and runs icon-cache refresh if the host has `gtk-update-icon-cache` (best-effort, non-fatal if absent).
- Test surface: `tests/test_install.py` (T2) asserts icon lands at the expected path in a tmp dest.

### W4 — Tool-parity fixes (audit #6, #14)

**T1 · CLAUDE_CONFIG_DIR probe/fallback**
- Files: `cld.py` (or `command_router.py`/`ClaudeAdapter` if W5-arch-deepening's router seam owns this by then — check current owner before editing), `tests/test_cld.py`.
- Contract: per `docs/superpowers/specs/2026-07-01-claude-code-account-switcher-design.md` — verify `CLAUDE_CONFIG_DIR` is honored by the invoked `claude` binary (probe: spawn with env set, confirm the resulting session reads from that dir) before relying on it for per-invocation isolation; on failure, reject with an explicit "unsupported routing" error rather than silently running against the wrong account.
- Test surface: add probe-failure and probe-success cases to `tests/test_cld.py`.

**T2 · Claude notify-send fallback**
- Files: whichever module owns Claude's user-facing error path post-deepening (`ClaudeAdapter` in `command_router.py`, most likely), `tests/test_cld.py`.
- Contract: Claude adapter fires `notify-send` on failure, matching `cdx.py`'s existing behavior (currently `cdx.py:210` pre-deepening — re-locate post-merge).
- Test surface: mirror the existing Codex notify-send test for the Claude path.

---

## Architecture Decisions

- **Install module owns all host-integration writes (wrappers, desktop entry, icon)** — one seam, not three. Deletion test: removing `install.py` scatters symlink/desktop/icon logic back across setup docs and manual steps (current state).
- **Symlink over wrapper-script generation** for W2/T2 — simpler seam, standard Python packaging convention (console_scripts-equivalent for a non-packaged repo).
- **Lint/type gate picked as ruff+mypy** — no existing convention in this repo to match; ruff+mypy is the current Python-ecosystem default, not a novel choice.

## Risks

- **W3/T1 needs W2/T1 to already exist** (autostart install folds into the same install module) — sequential dependency, not parallel with W2.
- **W4 module ownership may shift** depending on exactly what `plan/arch-deepening` W5 (`CommandRouter`) leaves in `cld.py` vs `ClaudeAdapter` — re-verify file targets after precondition #1/#2 land, same caveat as the arch-deepening spec's "re-verify mapping after reconcile."
- **`.desktop` Exec rewrite (W3/T1)** touches the one place external tests (`tests/test_packaging_desktop_entry.py`) already assert on a literal path — must update that test in the same task, not leave it asserting the old hardcoded string.
