---
name: afx-heads
description: Centralize Artifex per-project state (.artifex/ directories) under a single tracked repository. Use this skill when the user wants to bootstrap the shared project heads repo, onboard a project into it, run cross-project queries over ticket databases, or relocate the heads repo to a new path. All four subcommands (init, link, query, relocate) are available.
---

# Artifex Project Heads Skill

**For Claude Code AI Assistant**

This skill manages the **Project Heads** pattern: a single, tracked git repository that holds the `.artifex/` state for every Artifex project on the machine. Each real project keeps its own `.gitignore`d symlink pointing into the heads repo, so ticket databases, state notes, decisions, and lessons are versioned exactly once, in a place where they can be browsed, committed, and queried across projects.

## Why a central head repo?

Without heads, every project has its own `.artifex/tickets.db`, `.artifex/state/`, and `.artifex/lessons/` directory. That works, but it has three drawbacks:

1. **No cross-project visibility.** You cannot ask "show me every IN_PROGRESS ticket across all my projects" without writing ad-hoc SQL loops.
2. **Losing state is easy.** `.artifex/` is gitignored at the project level (or partially tracked). A `rm -rf` of a worktree or a missed commit can evaporate ticket history.
3. **No single source of truth for state decisions.** DECISIONS.md and PROGRESS.md live in each project repo, invisible when you are working in another repo.

The Project Heads pattern fixes all three by pointing each project's `.artifex/` at a subdirectory inside `~/projects/afx-project-heads/<project-slug>/.artifex`. That directory is git-tracked, so every commit snapshots the ticket state at that moment. Individual project repos ignore `.artifex` entirely.

## Layout

```
~/projects/afx-project-heads/           ← the head repo (git-tracked)
├── .git/
├── .gitignore                          ← selective ignores (tmp/, traces/, pm-config.json)
├── README.md                           ← explains the pattern for future you
├── registry.json                       ← machine-readable list of linked projects
├── movira/
│   └── .artifex/
│       ├── tickets.db                  ← tracked (source of truth)
│       ├── state/ lessons/ evals/      ← tracked
│       └── tmp/ traces/                ← gitignored
└── another-project/
    └── .artifex/ ...

~/projects/movira/
├── .artifex → ~/projects/afx-project-heads/movira/.artifex   ← symlink
└── .gitignore                          ← contains a single line: .artifex
```

## Subcommands

| Subcommand | Status | Purpose |
|------------|--------|---------|
| `init` | **Available** (ticket 11.1) | Bootstrap the head repo |
| `link` | **Available** (ticket 11.2) | Move a project's `.artifex` into the head repo and create the symlink |
| `query` | **Available** (ticket 11.3) | Cross-project SQL with every linked `tickets.db` auto-attached |
| `relocate` | **Available** (ticket 11.4) | Move the head repo to a new path and re-link every project |

All four subcommands are implemented. The script does not fail silently — unknown subcommands and unknown flags print a clear error referencing `--help`.

## Conversational Routing

When the user is asking about Project Heads, decide which subcommand to invoke based on the shape of the request:

| User phrasing | Subcommand |
|---------------|-----------|
| "set up project heads", "bootstrap the heads repo", "create the central state repo" | `init` |
| "onboard this project", "link my project", "move .artifex into the heads repo", "centralize state for X" | `link` |
| "onboard every project under ~/projects", "sweep my projects directory" | `link --all <root>` |
| "show me all IN_PROGRESS tickets across projects", "cross-project ticket summary", "which projects have X tickets" | `query` |
| "move my heads repo to /new/path", "I reorganized ~/projects and need to re-point symlinks" | `relocate` |
| "my .artifex symlink is broken after I moved the heads repo" | `relocate` (the sweep will re-point every registered symlink) |

If the user is unsure which subcommand they need, ask one clarifying question — the most common confusion is between `link` (onboarding one project for the first time) and `relocate` (the heads repo already exists and has moved).

## How to Run This Skill

### Bootstrapping the head repo (`init`)

When the user asks to "set up project heads", "create the afx-heads repo", or "bootstrap the central state repo", walk them through:

1. Confirm the target path. The default is `~/projects/afx-project-heads`; if the user wants a different location, pass it as a positional argument.
2. **Always preview with `--dry-run` first:**
   ```bash
   ~/.claude/skills/afx-heads/afx-heads.sh init --dry-run
   ```
   Show them what will be created (the directory, `.git/`, `README.md`, `.gitignore`, `registry.json`).
3. If they approve, run for real:
   ```bash
   ~/.claude/skills/afx-heads/afx-heads.sh init
   ```
4. Point them at the next step: "Now you can run `afx-heads link <project-dir>` (coming in ticket 11.2) to onboard your first project."

**Edge cases init handles:**

- **Path already exists as a valid head repo** (has `.git/` and `registry.json` with the right schema): idempotent no-op. Prints "already initialized" and exits 0.
- **Path exists with unrelated files**: refuses with exit 1 unless `--force`. With `--force`, the head repo files are written **alongside** the existing content — `--force` never deletes anything. Warn the user about the mixed directory.
- **Dry run**: nothing is written, but the preview shows exactly what would happen.

### Onboarding a project (`link`)

When the user asks to "link my project", "move my .artifex into the heads repo", or "onboard X into the central state repo":

1. Confirm which project — the absolute path is best. The slug defaults to the project's basename.
2. Always preview first:
   ```bash
   ~/.claude/skills/afx-heads/afx-heads.sh link <project-path> --dry-run
   ```
3. If the user approves, run for real. Pass `--force` only if the project's git is dirty and the user explicitly acknowledges it. Pass `--slug=<name>` if the user wants a specific slug.
4. After linking, point the user at the follow-up: commit the new entry in the heads repo when they are ready.

**Sweep mode (`link --all <root>`)** is useful when onboarding multiple projects at once — for example, "move every project under ~/projects into the heads repo". Sweep mode auto-excludes the heads repo itself, skips already-linked projects with a no-op, and reports aggregate counts at the end.

### Cross-project SQL (`query`)

When the user asks to "show me IN_PROGRESS tickets across every project", "compare ticket counts", or "run a cross-project report":

1. Run the query using one of two forms:
   - Explicit aliases: `afx-heads query "SELECT COUNT(*) FROM movira.stories WHERE status='TO_DO'"`
   - `{ALL_PROJECTS: ...}` expansion: `afx-heads query "{ALL_PROJECTS: SELECT id, title, status FROM stories WHERE status='IN_PROGRESS'}"`. The expansion prepends a literal `project` column with the slug and UNION-ALLs across every registered project.
2. Use `--include=<slug1,slug2>` / `--exclude=<slug1,slug2>` to restrict the scope. `--include` wins if both are passed.
3. Use `--format=json` or `--format=csv` when feeding the output into another tool.

Hyphenated slugs are automatically converted to underscore aliases inside SQL (`movira-platform` → `movira_platform`). The mapping is printed before the query runs.

### Moving the heads repo (`relocate`)

When the user asks to "move my heads repo to a new location" or "re-point every project symlink":

1. Preview first:
   ```bash
   ~/.claude/skills/afx-heads/afx-heads.sh relocate /new/path --dry-run
   ```
2. Run for real. Every symlink whose target is the current head root gets re-pointed at the new location. Symlinks pointing elsewhere (manually changed, or the user replaced one with a regular directory) are classified as **ORPHAN** and are not touched — the user has to fix those by hand.
3. Registry's `head_root` is updated to the new absolute path. Link entries' `head_path` fields (the per-slug subdirectory names) are unchanged.

## Flags Reference

| Flag | Purpose |
|------|---------|
| `--dry-run` | Preview only, touches nothing |
| `--force` | Proceed even if the target path exists with unrelated files (does NOT delete anything) |
| `--help` / `-h` | Usage |

## Interpreting Output

After `init` runs, confirm with the user:

- The head repo path (absolute)
- That `git init` ran and the branch is `main`
- That `README.md`, `.gitignore`, and `registry.json` were created
- That the repo has **no initial commit** — the user is expected to commit when they are ready (typically after their first `afx-heads link`)

## Idempotency Guarantees

`init` is safe to re-run:

- An existing valid head repo short-circuits to a no-op.
- `--force` is only required when the target already has unrelated files; it does not delete them.
- No initial commit is ever made automatically, so re-running does not pollute git history.

## Version

1.1.0 (tickets 11.1–11.4 — init, link, query, relocate)
