type: meta

slug: packaging-hygiene

topic: Packaging & hygiene: install/uninstall module, portable packaging, lint/type gate, remaining tool-parity gaps via run-plan-codex

exec_mode: run-plan-codex

scheduler: dag-parallel

land_mode: pr

base_branch: main

gate0_cmd: python3 -m pytest tests/ -q

codex_impl_effort: high

codex_review_effort: low

status_enum: BLOCKED PENDING WORKING WRITTEN REVIEWED COMMITTED

truth_rule: this file is ground truth for INTENT (goals/directions/task list); git is ground truth for DONE-NESS — reconcile statuses against git, never trust a COMMITTED line with no commit

refresh_how: native Read of THIS file — NEVER ctx_execute/ctx_search

refresh_what: meta + every goal/methodology/intent/direction/gated record + all WORKING and BLOCKED tasks

refresh_when:
wave boundary
any task status change
after a compaction system-reminder
before calling advisor

update_rule: on status change, rewrite that task's single line in place; append new tasks/session_memory as new lines

report_rule: at session end emit brief: done / blocked / deferred / canceled — NO next action

executor_notes: run-plan-codex reads: meta.base_branch, meta.gate0_cmd, meta.codex_impl_model/effort, meta.codex_review_model/effort; per task t.wave, t.files, t.blocked_by, t.seat. It IGNORES t.deps and meta.scheduler — cross-wave ordering is enforced purely by t.wave. base_branch verified 2026-07-02: main tip dbf9858 already carries both plan/arch-deepening (10/10 waves) and the quick-fix trio (audit #4/#7/#9), landed via finishing-a-development-branch this session — no sentinel/reconcile needed, base is resolved.


type: goal

text: 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). Source: docs/architecture-gap-audit-2026-07-02.md Critical Product Gaps #6, #11-14; Repo Hygiene #34-40.


type: methodology

text: Contract-first, behavior-preserving except the explicit fixes named per task. No implementation bodies — implementer derives them from the repo. Design spec: docs/specs/2026-07-02-packaging-hygiene-design.md.


type: intent

text: Install module owns all host-integration writes (wrappers, desktop entry, icon) — one seam, not three, so removing install.py doesn't scatter symlink/desktop/icon logic back across manual setup steps (current state).


note: Spec preconditions #1-#4 (arch-deepening merged, quick-fix trio merged, main reconciled, base_branch sentinel) all independently re-verified TRUE at plan-authoring time (main=dbf9858, 182/182 tests pass) — spec's own text is stale (written before the land), base_branch set directly to 'main', no gated record needed.


note: Spec W1/T1 (gitignore .worktrees//.wt-*-int//tmp/) is ALREADY DONE — landed in commit 518a8fd this session as a side effect of fixing a dirty-main-worktree land blocker. Confirmed via `cat .gitignore`. NOT a task in this plan.


note: Spec W4/T2 (Claude notify-send fallback) is PARTIALLY DONE by arch-deepening's CommandRouter refactor: ToolAdapter.notify_fallback() (command_router.py:242) is already generic and called from CommandRouter.main() (command_router.py:376) for BOTH CodexAdapter and ClaudeAdapter — no code fix needed. Only gap: tests/test_cdx.py:357 asserts the Codex-path notify-send call; no equivalent assertion exists for the Claude path. w4.t2 below is a test-coverage-only task, not a bug fix — do not let the implementer re-derive fallback logic that already exists.


note: W4/T1 (CLAUDE_CONFIG_DIR probe) has NO existing implementation — tests/test_cld.py and tests/test_command_router.py only assert the env var is SET in the exec env, never that the `claude` binary actually honors it. Genuine gap, real task.


type: task

desc: Lint/type gate: adopt ruff+mypy (industry standard, no existing convention to match). ruff.toml (or pyproject.toml [tool.ruff]) + mypy.ini, both runnable and passing (or documented per-line noqa/type:ignore, never blanket-ignored). No test surface (tooling) — document exact invocation in CONTEXT.md's existing conventions section (audit #34).

status: COMMITTED


desc: Install/uninstall module: single module owns host integration (audit #11/#37/#39). Contract: 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. Idempotent: install() twice does not duplicate entries or error; uninstall() on clean host is a no-op not an error. For this wave: symlink cdx.py/cld.py into dest_bin only (desktop/icon land in W3, same file, later waves). Disjoint from w1.t1/w4.t1 -> parallel.


desc: CLAUDE_CONFIG_DIR probe/fallback (audit #14). No existing implementation — tests only assert the env var is set, never that `claude` honors it. Contract: add ToolAdapter.verify_env_support(self, account_home: Path) -> bool (command_router.py) defaulting True (no probe); CodexAdapter inherits default; ClaudeAdapter overrides to spawn `claude` with CLAUDE_CONFIG_DIR=account_home set and confirm the resulting session reads from that dir. CommandRouter.main() calls adapter.verify_env_support(account_home) before execvpe; on False, print explicit 'unsupported routing' error to stderr and return 1 instead of exec'ing against the wrong account. Disjoint from w1.t1/w2.t1 -> parallel.


desc: Executable CLI wrappers (audit #11/#37): #!/usr/bin/env python3 shebang + mode 755 on cdx.py/cld.py/systray_codex_switcher.py (no logic change). install.py (w2.t1) symlinks these directly (prefer symlink over wrapper-script generation — simpler seam, standard non-packaged-repo convention) rather than generating separate wrapper scripts. Modifies install.py from w2.t1 -> sequential, wave 2.


desc: Claude notify-send test coverage (audit #6). Code is ALREADY generic (ToolAdapter.notify_fallback, command_router.py:242/376 fires for both adapters) — this is a test-gap fill, not a fix. Add a Claude-path assertion mirroring tests/test_cdx.py:357's Codex-path assertion (fallback_from unavailable -> notify-send popen call with cld's tool_name). Depends on w4.t1 (same test file, avoid conflicting edits) -> disjoint from w2.t2's files -> same wave, parallel.


desc: Runtime smoke test for wrappers (audit #39): unit tests monkeypatch exec/subprocess and never catch real wrapper breakage. At least one test invokes the installed (or symlinked-in-tmp) cdx/cld entry point as a real subprocess (not main() call) and asserts nonzero-but-graceful exit + stderr message on expected no-account-configured state — proves the wrapper is executable end to end, no OS-boundary mocking. Depends on w2.t2 (wrappers must be executable/symlinkable first). Disjoint from w3.t1's files -> same wave, parallel.


desc: Portable autostart entry (audit #12): Exec= path rewritten at install time to the actual invocation path — install.py template-substitutes at install(), never a hardcoded /home/user/... string committed to the repo. packaging/codex-account-switcher.desktop in the repo carries a placeholder (or is generated), never a machine-local absolute path. Extend tests/test_packaging_desktop_entry.py: assert the INSTALLED .desktop file's Exec= matches the actual install destination in a tmp dest, not a literal from the source file (current test already computes expected_exec dynamically via git-common-dir — extend it to also cover the installed-copy path, not just the committed template). Modifies install.py (from w2.t1/w2.t2) -> depends on both, sequential from w2.t2's install.py edit. Disjoint from w2.t3's files -> same wave, parallel.


desc: Icon install automation (audit #13): install() copies icons/codex-account-switcher.svg to ~/.local/share/icons/hicolor/scalable/apps/ and runs gtk-update-icon-cache if the host has it (best-effort, non-fatal if absent). tests/test_install.py asserts icon lands at the expected path in a tmp dest. Modifies install.py again -> same file as w3.t1, must be sequential -> wave 4, depends on w3.t1.


note: run-plan-codex.js ignores t.deps and meta.scheduler; ordering is by t.wave only. deps here document real dependencies for humans/fix-rot. Within-wave tasks are file-disjoint (verified against both create and modify lists) so same-wave parallel is safe: w1.t1/w2.t1/w4.t1 (wave1), w2.t2/w4.t2 (wave2), w2.t3/w3.t1 (wave3).


note: install.py is the highest-file-contention target — created w2.t1, modified w2.t2/w3.t1/w3.t2 across 4 separate waves purely because same-wave same-file is forbidden, not because the edits are semantically dependent (icon automation and autostart-Exec-rewrite are independent features that happen to share a file). If review finds this waterfalls too much wall-clock, the fix is splitting install.py into install_wrappers.py/install_desktop.py/install_icons.py sub-modules re-exported from install.py — NOT collapsing waves and risking a same-wave merge conflict.