Initial public release
A chezmoi-based fleet-dotfiles template for macOS workstations: - Two-way auto-sync via launchd watcher + 5-min puller - Mesh SSH via modify_authorized_keys driven by .chezmoidata/fleet.yaml - age-encrypted secrets file - Bundled Claude Code agentic team (11 agents) + /lite + /lite-sub commands - Verify-before-claiming Stop hook - Generic statusline + project-boundary validate-path hook - Reference launchd plist for cross-fleet task-durations aggregation (companion repo: gitea.tojo.team/cardinale/task-durations) - AGENTS.md walks an agent through the entire setup Q&A interactively - docs/ covers architecture, security model, fleet onboarding
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# CLAUDE.md.example — fleet-relevant sections only
|
||||
|
||||
This file is a **redacted template**. The real `CLAUDE.md` lives at `~/.claude/CLAUDE.md` per machine and gets quite long with personal infrastructure notes (servers, registrar API patterns, project-specific rules). The sections below are the parts that are *generic* to anyone using this fleet template.
|
||||
|
||||
Copy to `dot_claude/CLAUDE.md.tmpl` in your fork (or just `dot_claude/CLAUDE.md` for a non-templated file) and add your own personal sections on top.
|
||||
|
||||
---
|
||||
|
||||
# Time Estimates — anchor on history, not gut feel
|
||||
|
||||
Agents reflexively quote "this'll take 10 minutes" for things that take 30 seconds and "30 seconds" for things that take an hour. Before stating any duration estimate, anchor on real history instead of guessing.
|
||||
|
||||
```bash
|
||||
~/.claude/scripts/task-durations/estimate.sh \
|
||||
[--files N] [--subagents] [--project X] [--skill Y] [--recent-days 30] \
|
||||
[--fleet] [--machine NAME]
|
||||
```
|
||||
|
||||
Output is `n / p50_s / p90_s / p99_s` in seconds.
|
||||
|
||||
**Default scope:** this machine's local history (`~/.local/share/task-durations/local.parquet`).
|
||||
**`--fleet`:** unions all fleet machines via Hive-partitioned shards at `hosts/host=<name>/tasks.parquet`.
|
||||
**`--machine NAME`:** filter the fleet view to one host (uses `hostname -s`).
|
||||
|
||||
The corpus is built from each machine's `~/.claude/projects/**/*.jsonl` (one row per user-prompt-to-next-prompt span). A Stop hook on every session keeps the local corpus fresh; a launchd job (`com.taskdurations.pull-fleet`, plist included in this template) keeps fleet shards fresh.
|
||||
|
||||
Rules:
|
||||
|
||||
- **Start with the global pool.** Quote a p50 + p90 from the unfiltered run before adding filters.
|
||||
- **Add filters only when `n >= ~20`.** Below that, percentiles are noise.
|
||||
- **Don't cite a single number.** Quote the range: "p50 ~2 min, p90 ~7 min" beats "this'll take 5 min."
|
||||
- **Skip the script for trivial estimates.** Read-a-file, rename, one-liner fix — gut feel is fine.
|
||||
|
||||
# Fleet Sync (chezmoi auto-sync)
|
||||
|
||||
This machine has TWO-WAY auto-sync for `~/.claude/` and the chezmoi `docs/` directory with the fleet's git remote. Managed-list files propagate to the fleet within ~7 minutes via two launchd jobs:
|
||||
|
||||
- **Watcher** (`com.chezmoi.claude-watcher`): runs `chezmoi-auto-sync.sh` whenever a watched path changes (~2 s debounce). The script does `git pull --rebase`, `chezmoi add` for each managed path, then commits + pushes any uncommitted source-dir changes.
|
||||
- **Puller** (`com.chezmoi.claude-puller`): runs `chezmoi update --force` every 5 minutes to pull updates from the other fleet machines.
|
||||
|
||||
## CRITICAL: Silent-revert hazard — check the managed list BEFORE editing anything in `~/.claude/`
|
||||
|
||||
The puller reverts every destination file to match its chezmoi source. If you edit a file that is NOT in the watcher's managed list, your edit lives for up to 5 minutes, then silently gets wiped — no error, no warning.
|
||||
|
||||
**Managed files** (the watcher captures edits at these paths):
|
||||
|
||||
- `~/.claude/CLAUDE.md`
|
||||
- `~/.claude/agents/` (recursive)
|
||||
- `~/.claude/settings.json`
|
||||
- `~/.claude/commands/` (recursive)
|
||||
- `~/.claude/hooks/` (recursive)
|
||||
- `~/.claude/statusline-command.sh`
|
||||
- `~/.claude/scripts/task-durations/extract.py`
|
||||
- `~/.claude/scripts/task-durations/estimate.sh`
|
||||
- `~/.claude/scripts/task-durations/pull-fleet.sh`
|
||||
- `~/.local/bin/chezmoi-auto-sync.sh` — the watcher script itself
|
||||
- `~/Library/LaunchAgents/com.chezmoi.claude-watcher.plist` — chezmoi-templated; auto-reloads on plist change
|
||||
- `~/Library/LaunchAgents/com.chezmoi.claude-puller.plist` — chezmoi-templated; auto-reloads on plist change
|
||||
- `~/Library/LaunchAgents/com.taskdurations.pull-fleet.plist` — chezmoi-templated; runs pull-fleet.sh every 300 s
|
||||
|
||||
The authoritative list of `chezmoi add` paths lives in `~/.local/bin/chezmoi-auto-sync.sh`. To manage a new path: edit that script, run `chezmoi add ~/path/to/new` once manually to seed the source, and update this section so future-you finds it accurate.
|
||||
|
||||
## Adding a new fleet machine
|
||||
|
||||
1. Generate an SSH identity key on the new machine: `ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519`
|
||||
2. Add the new machine to `.chezmoidata/fleet.yaml` with its hostname, user, and pubkey
|
||||
3. Run `chezmoi init https://<your-gitea-or-github>/<your-fork>.git` on the new machine, supply your age private key, then `chezmoi apply`
|
||||
4. The watcher + puller launchd jobs auto-load via the `run_onchange` script
|
||||
|
||||
# Verify Before Claiming
|
||||
|
||||
For substantive work (multi-file changes, system configuration, architecture decisions, tool/library recommendations): WebSearch or check the system before making factual claims about versions, APIs, maintenance status, or system state. Never cite training data as authoritative for anything that changes over time.
|
||||
|
||||
For simpler work: if you make a factual claim you haven't verified, mark it `[unverified]` inline. Example: *"React 19 supports this [unverified]"*.
|
||||
|
||||
A pre-bundled Stop hook (`~/.claude/hooks/verify-before-claiming.py`, included in this template) catches hedging language ("likely", "could be", "might") missing an `[unverified]` tag and prompts a revision before the turn ends.
|
||||
|
||||
# Background Monitors — prefer over `sleep`
|
||||
|
||||
When watching a long-running process, build, deploy, smoke test, or any external thing whose state changes over time: use the Monitor tool, not `sleep`-and-poll. `sleep N` blocks the conversation, burns context with no insight, and tells you nothing about whether the thing you're waiting on is making progress, stuck, or already done.
|
||||
|
||||
(See your Claude Code docs for the Monitor tool API.)
|
||||
@@ -0,0 +1,19 @@
|
||||
# ~/.gitconfig — copy to `dot_gitconfig` in your fork and replace the
|
||||
# placeholders with your name/email.
|
||||
|
||||
[user]
|
||||
email = <YOUR_EMAIL>
|
||||
name = <YOUR_NAME>
|
||||
|
||||
# git-lfs (optional — only useful if you commit binary assets)
|
||||
# [filter "lfs"]
|
||||
# clean = git-lfs clean -- %f
|
||||
# smudge = git-lfs smudge -- %f
|
||||
# process = git-lfs filter-process
|
||||
# required = true
|
||||
|
||||
[init]
|
||||
defaultBranch = main
|
||||
|
||||
[pull]
|
||||
rebase = false
|
||||
@@ -0,0 +1,45 @@
|
||||
# Per-machine secrets, sourced by .zshrc on shell start.
|
||||
# This file is encrypted via age before being committed to chezmoi —
|
||||
# the live disk copy lives at ~/.config/fleet-dotfiles/secrets.env.
|
||||
#
|
||||
# To enable encryption on a real fleet:
|
||||
# 1. Place this file at ~/.config/fleet-dotfiles/secrets.env
|
||||
# 2. chmod 600 ~/.config/fleet-dotfiles/secrets.env
|
||||
# 3. Replace placeholder values with real ones
|
||||
# 4. chezmoi add --encrypt ~/.config/fleet-dotfiles/secrets.env
|
||||
# (chezmoi auto-renames it to encrypted_private_secrets.env.age in source)
|
||||
# 5. The auto-sync watcher commits + pushes the encrypted version on edit
|
||||
#
|
||||
# Never commit the unencrypted version. Variables you don't use can be
|
||||
# deleted; the list below is illustrative of what a real fleet might carry.
|
||||
|
||||
# ───────── Cloudflare ─────────
|
||||
# Account ID + API tokens for Pages deploys, Workers, DNS API
|
||||
export CLOUDFLARE_ACCOUNT_ID=""
|
||||
export CLOUDFLARE_API_KEY=""
|
||||
export CLOUDFLARE_EMAIL=""
|
||||
|
||||
# ───────── Domain registrar (Porkbun) ─────────
|
||||
export PORKBUN_API_KEY=""
|
||||
export PORKBUN_SECRET_KEY=""
|
||||
|
||||
# ───────── Tailscale (for fleet access ACLs / DNS API) ─────────
|
||||
export TAILSCALE_API_KEY=""
|
||||
|
||||
# ───────── HuggingFace ─────────
|
||||
export HF_TOKEN=""
|
||||
export HUGGINGFACE_TOKEN=""
|
||||
|
||||
# ───────── LLM API providers ─────────
|
||||
export OPENAI_API_KEY=""
|
||||
export ANTHROPIC_API_KEY=""
|
||||
export GEMINI_API_KEY=""
|
||||
|
||||
# ───────── Gitea (this template's host) ─────────
|
||||
export GITEA_URL=""
|
||||
export GITEA_USER=""
|
||||
export GITEA_TOKEN=""
|
||||
|
||||
# ───────── Anything else ─────────
|
||||
# Add per-service tokens here. Naming convention: SERVICE_PURPOSE_KIND
|
||||
# e.g. STRIPE_LIVE_SECRET_KEY, DISCORD_BOT_TOKEN
|
||||
@@ -0,0 +1,41 @@
|
||||
# SSH config — chezmoi-templated so each machine renders its own version.
|
||||
#
|
||||
# The {{ if ne .chezmoi.hostname "<HOST>" }} guards prevent a machine
|
||||
# from generating a Host stanza for itself (which would loop back).
|
||||
#
|
||||
# To use: copy to private_dot_ssh/config.tmpl in your fork, replace
|
||||
# the placeholder host aliases / hostnames with your real fleet, and
|
||||
# replace the <USERNAME> tokens with the matching user from fleet.yaml.
|
||||
|
||||
# ── Fleet machines ──────────────────────────────────────────────
|
||||
|
||||
{{ if ne (lower .chezmoi.hostname) "laptop1" }}
|
||||
Host laptop1
|
||||
HostName laptop1
|
||||
User <USERNAME_FOR_LAPTOP1>
|
||||
IdentityFile ~/.ssh/id_ed25519
|
||||
StrictHostKeyChecking accept-new
|
||||
{{ end }}
|
||||
|
||||
{{ if ne (lower .chezmoi.hostname) "laptop2" }}
|
||||
Host laptop2
|
||||
HostName laptop2
|
||||
User <USERNAME_FOR_LAPTOP2>
|
||||
IdentityFile ~/.ssh/id_ed25519
|
||||
StrictHostKeyChecking accept-new
|
||||
{{ end }}
|
||||
|
||||
{{ if ne (lower .chezmoi.hostname) "desktop" }}
|
||||
Host desktop
|
||||
HostName desktop
|
||||
User <USERNAME_FOR_DESKTOP>
|
||||
IdentityFile ~/.ssh/id_ed25519
|
||||
StrictHostKeyChecking accept-new
|
||||
{{ end }}
|
||||
|
||||
# ── External hosts (servers, etc.) ──────────────────────────────
|
||||
|
||||
# Host my-vps
|
||||
# HostName vps.example.com
|
||||
# User root
|
||||
# IdentityFile ~/.ssh/id_ed25519_vps # encrypt via `chezmoi add --encrypt`
|
||||
@@ -0,0 +1,27 @@
|
||||
# Skeleton .zshrc rendered by chezmoi. Copy to dot_zshrc.tmpl in your fork
|
||||
# and add your own exports / PATH / completions / aliases.
|
||||
#
|
||||
# The single critical line is the one that sources the encrypted secrets
|
||||
# file at the bottom — don't remove that, or your fleet's API tokens
|
||||
# won't load on shell start.
|
||||
|
||||
# ───────── PATH ─────────
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
|
||||
|
||||
# ───────── Editor / pager ─────────
|
||||
export EDITOR="nvim"
|
||||
export PAGER="less"
|
||||
|
||||
# ───────── Completions / shell tools (uncomment what you use) ─────────
|
||||
# [ -s "{{ .chezmoi.homeDir }}/.bun/_bun" ] && source "{{ .chezmoi.homeDir }}/.bun/_bun"
|
||||
# eval "$(starship init zsh)"
|
||||
# eval "$(zoxide init zsh)"
|
||||
|
||||
# ───────── Aliases ─────────
|
||||
alias ll="ls -lah"
|
||||
alias g="git"
|
||||
|
||||
# ───────── Fleet secrets (must come last so per-key exports above can be overridden) ─────────
|
||||
# Loaded from the age-encrypted file managed by chezmoi.
|
||||
[[ -f "${HOME}/.config/fleet-dotfiles/secrets.env" ]] && source "${HOME}/.config/fleet-dotfiles/secrets.env"
|
||||
Reference in New Issue
Block a user