Files
ansible-arch/docs/superpowers/plans/2026-07-28-ansible-reconciliation.md
Tuan-Dat Tran 753bdfd403 docs: add implementation plan for ansible-arch reconciliation
Also folds in the dead handlers/main.yaml finding discovered while writing the plan.
2026-07-28 08:39:18 +02:00

13 KiB

Ansible-arch ↔ Machine Reconciliation Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Bring the ansible-arch repo and the machine it manages back into alignment: commit the in-flight working-tree changes, fix the become=True global-default bug that caused root-owned dotfiles symlinks, and codify already-installed-but-undeclared packages into the right roles.

Architecture: No new files or abstractions. This is a reconciliation pass over existing roles/*/tasks/*.yml files and ansible.cfg. Every privileged task in the repo already carries an explicit become: true — the audit confirmed the only structural bug is the global default in ansible.cfg, so that fix is a one-line change, not a per-task sweep. Verification is ansible-playbook --syntax-check / --check --diff where the modules support check mode, and one real ansible-playbook site.yml run at the end (safe: pacman tasks are no-ops for already-installed packages, dotfiles-role symlink recreation is idempotent by design).

Tech Stack: Ansible, Arch Linux (pacman, AUR via kewlfft.aur + paru), GNU Stow.


Task 1: Commit the in-flight working-tree diff

Files:

  • Modify (already modified in working tree, no further edits needed): dotfiles/hypr/.config/hypr/hyprland.conf, roles/ai/tasks/main.yaml, roles/common/defaults/main.yml, roles/common/tasks/main.yml, roles/devops/tasks/main.yml, roles/dotfiles/defaults/main.yml, roles/dotfiles/tasks/main.yml, roles/hyprland/tasks/main.yml, roles/multimedia/tasks/main.yml
  • Add (untracked): dotfiles/hypr/.config/hypr/hyprlock.conf, dotfiles/hypr/.config/hypr/monitors.sh, dotfiles/kanshi/, dotfiles/starship/

This working-tree diff already reflects the intended current state (verified during brainstorming — it's the hyprland.conf tweaks, new dotfiles stow packages, and new role packages that led to this reconciliation). Nothing to write here, just commit it.

  • Step 1: Review the diff one more time
git status
git diff --stat

Expected: the same 9 modified files and 4 untracked paths listed in the design spec's "Context" section.

  • Step 2: Stage and commit
git add dotfiles/hypr/.config/hypr/hyprland.conf \
        dotfiles/hypr/.config/hypr/hyprlock.conf \
        dotfiles/hypr/.config/hypr/monitors.sh \
        dotfiles/kanshi/ dotfiles/starship/ \
        roles/ai/tasks/main.yaml \
        roles/common/defaults/main.yml \
        roles/common/tasks/main.yml \
        roles/devops/tasks/main.yml \
        roles/dotfiles/defaults/main.yml \
        roles/dotfiles/tasks/main.yml \
        roles/hyprland/tasks/main.yml \
        roles/multimedia/tasks/main.yml
git commit -m "feat: formalize in-flight hyprland/dotfiles/role changes"
git status

Expected: git status shows "nothing to commit, working tree clean" (modulo files touched by later tasks).


Task 2: Fix the become=True global default

Files:

  • Modify: ansible.cfg:11-15

Context: ansible.cfg currently has:

[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False

Every task across every role that actually needs root (pacman:, systemd:, user:, getent:, chsh) already has an explicit become: true set on it — confirmed by reading every roles/*/tasks/*.yml file. Every task that must run unprivileged (AUR installs via kewlfft.aur.aur, the paru makepkg bootstrap, oh-my-zsh install, dotfiles stow/file tasks, blockinfile edits to .zshrc/.profile) already correctly omits become. The global become = True default was silently overriding all of those correctly-unprivileged tasks to root — that's why ~/.config/hypr, ~/.config/zellij, and ~/.config/starship.toml symlinks are owned by root.

So the fix is exactly one line.

  • Step 1: Remove the global become default

Edit ansible.cfg, changing:

[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False

to:

[privilege_escalation]
become_method = sudo
become_user = root
become_ask_pass = False
  • Step 2: Syntax-check the playbook
ansible-playbook site.yml --syntax-check

Expected: playbook: site.yml with no errors.

  • Step 3: Dry-run the parts that support check mode
ansible-playbook site.yml --check --diff

Expected: pacman/systemd/user tasks report no changes (packages already installed, services already enabled). The stow/command tasks in the dotfiles role will show as skipped (partial check-mode support) rather than simulate the symlink recreation — that's expected and covered by the real run in Task 6.

  • Step 4: Commit
git add ansible.cfg
git commit -m "fix: stop forcing become=True globally, rely on per-task become"

Task 3: Add missing packages to the ctf role

Files:

  • Modify: roles/ctf/tasks/main.yml

Current content:

# tasks file for ctf
- name: Install CTF tools (official repos)
  become: true
  pacman:
    name:
      - wireshark-qt
      - nmap
      - openbsd-netcat
      - binwalk
      - gobuster
    state: present

gitleaks, picocom, and python-pyserial are official-repo packages. nuclei-bin is AUR-only.

  • Step 1: Add the official-repo packages

Edit roles/ctf/tasks/main.yml to:

# tasks file for ctf
- name: Install CTF tools (official repos)
  become: true
  pacman:
    name:
      - wireshark-qt
      - nmap
      - openbsd-netcat
      - binwalk
      - gobuster
      - gitleaks
      - picocom
      - python-pyserial
    state: present

- name: Install CTF AUR packages
  kewlfft.aur.aur:
    name: "{{ item }}"
    use: "{{ aur_helper | default('paru') }}"
    state: present
  loop:
    - nuclei-bin
  • Step 2: Syntax-check
ansible-playbook site.yml --syntax-check

Expected: no errors.

  • Step 3: Dry-run just this role
ansible-playbook site.yml --check --diff --tags ctf

Expected: pacman task reports ok (all packages already installed, since the audit confirmed they're present via pacman -Qeq). The AUR task will show as skipped in check mode (module doesn't support check mode) — that's fine, it's covered by the Task 6 real run.

  • Step 4: Commit
git add roles/ctf/tasks/main.yml
git commit -m "feat(ctf): add gitleaks, nuclei-bin, picocom, python-pyserial"

Task 4: Add missing packages to the common role

Files:

  • Modify: roles/common/tasks/main.yml:19-53

Current "Install base development tools" task (lines 19-53):

- name: Install base development tools
  become: true
  pacman:
    name:
      - base-devel
      - git
      - curl
      - wget
      - rsync
      - unzip
      - zip
      - less
      - man-db
      - texinfo
      - which
      - zsh
      - stow
      - ansible
      - zoxide
      - atuin
      - broot
      - bottom
      - hyperfine
      - tokei
      - git-delta
      - yazi
      - ueberzugpp
      - tealdeer
      - ledger
      - btop
      - htop
      - bind
      - socat
      - iperf3
    state: present

Add nano, vim, sshpass, smartmontools here. cargo-audit isn't a pacman package (it's installed via cargo install), so it goes in the separate "Install Rust toolchain and Python uv" task as a follow-up cargo step.

  • Step 1: Add nano, vim, sshpass, smartmontools

Edit the name: list in "Install base development tools" (roles/common/tasks/main.yml:22-51), adding after iperf3:

      - iperf3
      - nano
      - vim
      - sshpass
      - smartmontools
    state: present
  • Step 2: Add cargo-audit via cargo install

Current "Install Rust toolchain and Python uv" task (roles/common/tasks/main.yml:55-68):

- name: Install Rust toolchain and Python uv
  become: true
  pacman:
    name:
      - rust
      - eza
      - bat
      - ripgrep
      - fd
      - starship
      - cargo
      - cargo-binstall
      - uv
    state: present

Add a new task immediately after it:

- name: Install cargo-audit
  command: cargo install cargo-audit --locked
  args:
    creates: "{{ archsetup_user_home }}/.cargo/bin/cargo-audit"

No becomecargo install must run as the invoking user, installing into ~/.cargo/bin.

  • Step 3: Syntax-check
ansible-playbook site.yml --syntax-check

Expected: no errors.

  • Step 4: Dry-run
ansible-playbook site.yml --check --diff --tags common

Expected: pacman task ok, no changes (all four packages already installed per the audit). The cargo-audit command task will show as skipped in check mode — verified for real in Task 6 (cargo-audit is already on the machine, per the audit, so it'll be a no-op there too via the creates guard).

  • Step 5: Commit
git add roles/common/tasks/main.yml
git commit -m "feat(common): add nano, vim, sshpass, smartmontools, cargo-audit"

Task 5: Remove duplicate docker package from cs_student role

Files:

  • Modify: roles/cs_student/tasks/main.yml:1-17

devops role (roles/devops/tasks/main.yml:6) already declares docker. Current cs_student task:

# tasks file for cs_student
- name: Install CS student tools
  become: true
  pacman:
    name:
      - neovim
      - emacs
      - python
      - nodejs
      - npm
      - texlive-bin
      - texlive-basic
      - jupyter-nbconvert
      - docker
      - code
      - github-cli
    state: present
  • Step 1: Remove the docker line

Edit to:

# tasks file for cs_student
- name: Install CS student tools
  become: true
  pacman:
    name:
      - neovim
      - emacs
      - python
      - nodejs
      - npm
      - texlive-bin
      - texlive-basic
      - jupyter-nbconvert
      - code
      - github-cli
    state: present
  • Step 2: Syntax-check
ansible-playbook site.yml --syntax-check

Expected: no errors.

  • Step 3: Commit
git add roles/cs_student/tasks/main.yml
git commit -m "refactor(cs_student): drop duplicate docker package, already in devops role"

Task 6: Remove the dead duplicate handler file

Files:

  • Delete: roles/common/handlers/main.yaml

Context: roles/common/handlers/ contains both main.yml and main.yaml. Ansible's role loader (ansible/playbook/role/__init__.py:_load_role_yaml, confirmed by reading the installed Ansible source) searches extensions in the fixed order ['.yml', '.yaml', '.json'] and stops at the first match for handlers (allow_dir=False). That means main.yml — which already has become: true and is the one referenced correctly — is the only file Ansible ever loads. main.yaml is silently dead: it's never parsed, and its font-cache handler task (which lacks become: true and would break under the Task 2 fix if it were ever loaded) is orphaned.

Since main.yml is confirmed live and already correct, main.yaml is pure dead weight causing confusion about which handler is authoritative — remove it.

  • Step 1: Confirm main.yml is the active file
cat roles/common/handlers/main.yml

Expected output:

- name: Update font cache
  become: true
  command: fc-cache -fv
  • Step 2: Delete the dead duplicate
git rm roles/common/handlers/main.yaml
  • Step 3: Syntax-check
ansible-playbook site.yml --syntax-check

Expected: no errors (confirms main.yml alone still satisfies the notify: Update font cache handler reference in roles/common/tasks/main.yml).

  • Step 4: Commit
git commit -m "chore(common): remove dead duplicate handlers/main.yaml (main.yml is the one Ansible loads)"

Task 7: Real playbook run and final verification

This is the only step in the plan that touches live machine state beyond --check dry-runs. It's expected to be safe and mostly a no-op: every package involved is already installed (confirmed via pacman -Qeq during the audit), and the dotfiles role's "Remove existing config files before stowing" task unconditionally removes and recreates its symlink targets on every run by design — that's what fixes the root-ownership bug, with no manual rm/chown needed.

  • Step 1: Run the full playbook
ansible-playbook site.yml

Expected: playbook completes with failed=0. Some tasks report changed (dotfiles symlinks recreated, any packages that were pulled in by Tasks 3-4 but not yet actually reconciled by pacman state).

  • Step 2: Verify symlink ownership
ls -la ~/.config/hypr ~/.config/zellij ~/.config/starship.toml ~/.config/kanshi

Expected: all four owned by tudattr, not root.

  • Step 3: Verify no drift remains
ansible-playbook site.yml --check --diff
git status

Expected: second --check run reports no further changes (idempotent), git status shows a clean working tree.