{"type":"meta","schema":"session-state/v1","slug":"packaging-hygiene","topic":"Packaging & hygiene: install/uninstall module, portable packaging, lint/type gate, remaining tool-parity gaps via run-plan-codex","created":"2026-07-02","exec_mode":"run-plan-codex","scheduler":"dag-parallel","land_mode":"pr","base_branch":"main","gate0_cmd":"python3 -m pytest tests/ -q","codex_impl_model":"gpt-5.4-mini","codex_impl_effort":"high","codex_review_model":"gpt-5.4","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)."}
{"type":"direction","verbatim":"packaging-hygiene next?"}
{"type":"session_memory","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."}
{"type":"session_memory","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."}
{"type":"session_memory","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."}
{"type":"session_memory","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":"anchor","path":"docs/specs/2026-07-02-packaging-hygiene-design.md","what":"spec"}
{"type":"anchor","path":"docs/architecture-gap-audit-2026-07-02.md","what":"source audit (gaps #6, #11-14, #34-40)"}
{"type":"task","id":"w1.t1","wave":1,"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","deps":[],"blocked_by":[],"requires_decision":null,"files":["ruff.toml","mypy.ini","CONTEXT.md"],"blocker":null}
{"type":"task","id":"w2.t1","wave":1,"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.","status":"COMMITTED","deps":[],"blocked_by":[],"requires_decision":null,"files":["install.py","tests/test_install.py"],"blocker":null}
{"type":"task","id":"w4.t1","wave":1,"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.","status":"COMMITTED","deps":[],"blocked_by":[],"requires_decision":null,"files":["command_router.py","tests/test_command_router.py"],"blocker":null}
{"type":"task","id":"w2.t2","wave":2,"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.","status":"COMMITTED","deps":["w2.t1"],"blocked_by":["w2.t1"],"requires_decision":null,"files":["cdx.py","cld.py","systray_codex_switcher.py","install.py","tests/test_install.py"],"blocker":null}
{"type":"task","id":"w4.t2","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.","status":"COMMITTED","deps":["w4.t1"],"blocked_by":["w4.t1"],"requires_decision":null,"files":["tests/test_command_router.py"],"blocker":null}
{"type":"task","id":"w2.t3","wave":3,"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.","status":"COMMITTED","deps":["w2.t2"],"blocked_by":["w2.t2"],"requires_decision":null,"files":["tests/test_cli_smoke.py"],"blocker":null}
{"type":"task","id":"w3.t1","wave":3,"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.","status":"COMMITTED","deps":["w2.t1","w2.t2"],"blocked_by":["w2.t1","w2.t2"],"requires_decision":null,"files":["install.py","packaging/codex-account-switcher.desktop","tests/test_packaging_desktop_entry.py"],"blocker":null}
{"type":"task","id":"w3.t2","wave":4,"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.","status":"COMMITTED","deps":["w3.t1"],"blocked_by":["w3.t1"],"requires_decision":null,"files":["install.py","icons/codex-account-switcher.svg","tests/test_install.py"],"blocker":null}
{"type":"session_memory","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)."}
{"type":"session_memory","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."}
