Skip to content

Latest commit

 

History

History
76 lines (53 loc) · 7.34 KB

File metadata and controls

76 lines (53 loc) · 7.34 KB

AGENTS.md

This file provides guidance to coding agents (Claude Code, Codex, Copilot CLI, OpenCode, …) when working with code in this repository.

Read README.md first — it covers the user-facing flow: client roles, the role-layer table (bootstrap / base / server_hardening / terminal_dotfiles / personal / graphical_dotfiles / os_macos / coding_agents), and bootstrap/usage commands. This file only documents the non-obvious architecture and editing constraints that aren't in README.

What this repo is

Personal configuration monorepo driving three loosely-coupled subsystems:

  • Ansible (ansible/) — the active provisioning layer, shared across all hosts (Linux and macOS).
  • NixOS (nixos/) — declarative system config for the NixOS machines (artus, margo, jolly, …). Not every host uses it.
  • Dotfiles (dotconfig/) — raw config trees (fish/nvim/kitty/hypr/lf/broot) consumed by the Ansible roles via symlink.

bootstrap.sh is the green-field entrypoint; coding-agents/ is a generator that fans out one agent definition to four CLIs.

Ansible architecture (non-obvious bits)

  • Single entrypoint. ansible/site.yml loops over each host's host_roles list and applies roles individually, tagging each by role name. There is no per-host playbook — host identity comes from ansible/inventory/host_vars/<name>.yml, which sets host_roles plus host-specific vars.

  • Push vs. pull duality. Push (ansible-playbook -l <host>) uses the inventory. Pull (ansible-pull on-host) sets inventory_hostname=localhost, so host_vars are resolved via -e host_id=<name> and an explicit include_vars in site.yml's pre_tasks. When touching site.yml or host_var loading, keep both paths working.

  • Role layering intent. base is universally safe — never add firewall, hostname rewrite, swap, or other destructive defaults to it. Those belong in server_hardening. The terminal host profile (host_vars/terminal.yml) deliberately excludes personal and server_hardening so it's safe to apply on any unfamiliar VM.

  • confirm_overwrite=false by default. terminal_dotfiles skips symlinking over real config directories (fish/kitty/nvim/broot create their own on first launch). Pass -e confirm_overwrite=true to force; ignored under --check for safety.

  • group_vars/all.yml holds shared identity + cross-role config (git identity, home_dir computed from OS, repos_list, repo_root). Roles read these rather than redefining defaults.

  • Roles describe a clean install, not migrations. Never add tasks whose only purpose is removing/renaming legacy state that a fresh install wouldn't have (stale symlinks, renamed configs). One-time cleanups are done manually per host.

Validating changes: there are no tests/lint. Use --check --diff against a real host, or ./scripts/ansible_smoketest.sh. Subset by role with --tags <role>.

NixOS layer

README covers the high-level rules (live entrypoint is /etc/nixos/configuration.nix, host-specific behavior under nixos/machines/, shared desktop plumbing under nixos/shared/, jolly upgrades via next-boot not live-switch). Beyond that:

  • nixos/shared/ is split into base-system.nix, networking.nix, desktops/ (Hyprland), programs/, packages/, policy/, users/, hardware/. New cross-host plumbing belongs here.
  • Repo-root configuration.nix is a reference entrypoint, not authoritative for every machine. Machine-local hardware-configuration.nix stays on the host.
  • See docs/jolly.md for the recovery context behind the boot-vs-switch rule.

Hyprland is on a Lua config — hyprctl takes Lua, not the legacy syntax

dotconfig/hypr/hyprland.lua replaced hyprland.conf. This also changes every external caller, which is easy to miss: with a Lua config root, hyprctl's parser is Lua too.

  • hyprctl dispatch <name> <args>hyprctl dispatch '<lua expr>', e.g. hyprctl dispatch 'hl.dsp.window.float({ action = "toggle" })'. This applies to every dispatcher, including argument-less-looking ones like dpms on.
  • hyprctl keyword a:b vhyprctl eval 'hl.config({ a = { b = v } })' (keyword errors with "can't work with non-legacy parsers").
  • exec with window rules: hyprctl eval 'hl.exec_cmd([[cmd]], {workspace = [[3 silent]]})' — the bracket contents become the rule table value, and {silent = true} is rejected ("unknown effect").
  • hyprctl exits 7 on a parse error. Check it in scripts: the failure mode is otherwise completely silent (this is how the whole login autostart broke).

hl.dsp.dpms needs a table: hl.dsp.dpms({ action = "on" }). A bare string leaves action nil and turns the display off. On jolly that is not recoverable — the monitor is also the USB KVM hub, so ~90s without a DP signal makes it auto-switch input and take the keyboard, audio and ethernet with it. Never probe dpms against the real display; hyprctl output create headless gives you a throwaway monitor to test against.

coding-agents

A generator: one source file per agent fans out to four CLIs (Claude Code, Codex, Copilot CLI, OpenCode). The full guide is coding-agents/README.md — read it before creating or editing any agent or skill. The essentials:

  • Edit only coding-agents/source/<name>.md. It's opencode-flavored: YAML frontmatter + a markdown body that is the agent's prompt. build.py renders per-tool variants into coding-agents/generated/ (gitignored); the coding_agents Ansible role runs build.py then symlinks them into ~/.claude/agents/, ~/.config/opencode/agents/, ~/.codex/agents/, ~/.copilot/agents/. Never hand-edit generated/ — it's rebuilt every run.
  • mode: decides what you get. mode: subagent (default) → invoked by another agent via the Task/Agent tool, runs in its own context, can't pause for the user — use for delegated, self-contained jobs (a reviewer, a specialist). mode: primary → ALSO rendered as a Skill (/<name>) for Claude Code + Codex, runs in the main thread, can pause for sign-off / clarifying questions while still delegating to subagents — use for an interactive workflow (e.g. ic).
  • Permissions translate to real restrictions per tool. permission: { edit|bash|webfetch: allow|ask|deny }. A deny is enforced — e.g. edit: deny strips Edit/Write/NotebookEdit from the Claude variant, webfetch: deny strips WebFetch/WebSearch. Full translation table in coding-agents/README.md.
  • description should start with "Use when …" — it's what each tool reads to decide when to pick the agent.
  • Deleting source/<name>.md does not auto-remove the existing symlinks — clean those up by hand.
  • After editing, rebuild + resync: ansible-playbook ansible/site.yml -l <host> --tags coding_agents (or just python3 coding-agents/build.py to verify rendering without installing).

Conventions

Commit messages: scope prefix first, then short imperative summary — e.g. ansible:, nixos:, fish:, claude:, kvm:. Match the existing style in git log.

For simple, self-contained config changes (e.g. adding a package to a host, flipping a flag, bumping a value), commit the change yourself without being asked — stage only the files you touched, leave unrelated working-tree changes alone, and use the convention above. Surface anything non-trivial (architectural choices, destructive ops) before committing.