Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ See [`templates/README.md`](./templates/README.md) for conventions and per-categ
| [`design/explore-designs`](./ai/skills/design/explore-designs/) | Draft | Guides using Claude Design to explore design directions across your frontend stack |
| [`design/create-design-system`](./ai/skills/design/create-design-system/) | Placeholder | Design-system setup |
| [`design/design-handoff`](./ai/skills/design/design-handoff/) | Ready | Implements a Claude Design `.tar.gz` handoff into a real codebase — reconcile an existing design system or bootstrap a new one (tokens → shadcn/Tailwind v4 OKLCH, `/brand`, contrast + licensing gates) |
| [`repo/standardize-repo`](./ai/skills/repo/standardize-repo/) | Ready | Applies the [harmon-init](https://github.com/evanharmon1/harmon-init) Copier template's conventions to a repo — scaffold a new repo, adopt the template into an existing one, or audit drift from the standards. Bundles the authoritative repo-conventions catalog. |

## Inspired by Other Boilerplate Repos

Expand Down
102 changes: 102 additions & 0 deletions ai/skills/repo/standardize-repo/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
name: standardize-repo
description: >-
Apply the harmon-init Copier template's conventions (DevOps tooling, CI/CD, lint,
security, git hooks, Taskfile) to a repo. Use whenever the user wants to "apply
harmon-init", "scaffold a new repo with my conventions", "set up a new project",
"adopt the template", "bring this repo up to my standards", "standardize this repo",
or "audit this repo against my standards / check what's missing". Covers three
modes: scaffolding a brand-new/empty repo, retrofitting an existing repo with git
history, and auditing a repo for drift from the standards. Trigger it even if the
user doesn't say the word "skill".
allowed-tools: Bash, Read, Write, Edit, Grep, Glob, Task, WebFetch
---

# Standardize Repo (apply harmon-init conventions)

Bring a repo in line with the **harmon-init** Copier template — the shared baseline
of DevOps tooling, CI/CD, linting, secrets scanning, lefthook git hooks, and a
`Taskfile.yml` task runner. harmon-init is the **template** repo of harmon-stack
(siblings: harmon-devkit, harmon-dotfiles, harmon-ops, harmon-infra); this skill is
how an agent *consumes* that template to scaffold new repos or standardize existing
ones. harmon-init is NOT an application — it is used via
[Copier](https://copier.readthedocs.io/en/stable/), so the heavy lifting is
`copier copy` / `copier update`, not hand-copying files.

## Preconditions

Verify these before doing anything; stop and tell the user if one is unmet.

- **copier** installed — `copier --version` (needs `>= 9.4.0`, per `_min_copier_version`).
- **harmon-init** cloned locally at `~/git/harmon-init`. If missing:
`git clone https://github.com/evanharmon1/harmon-init ~/git/harmon-init`.
- **task** (go-task) on PATH — `task --version` — for the verification gate.
- **gh** authenticated (`gh auth status`) — only needed for the GitHub side-effect
steps (remote create, release init). Not required for local scaffolding.

## Mode routing

Detect the situation, then follow the matching reference file end to end.

| Situation | Mode | Reference |
| --- | --- | --- |
| Target dir is empty / does not exist yet (new project) | **new-repo** | `references/mode-new-repo.md` |
| Target is an existing repo **with git history** (retrofit) | **adopt-existing** | `references/mode-adopt-existing.md` |
| User says "audit" / "check" / "what's missing" / "bring up to standard" / drift report | **audit** | `references/mode-audit.md` |

If it is ambiguous (e.g. a non-empty dir that is not a git repo), ask the user which
mode they want rather than guessing — `copier copy` vs `copier update` behave very
differently.

## Cardinal copier rules (read before running any copier command)

These are load-bearing. Full rationale and edge cases in `references/copier-gotchas.md`.

- **Always pass `--vcs-ref=HEAD` when the template source is a local path.** Without
it, copier renders the **latest git tag** of harmon-init and silently ignores all
uncommitted *and* committed-but-untagged work. With it, copier auto-includes
dirty/untracked changes via a throwaway commit in a temp clone
(`DirtyLocalWarning`) — the working tree is never touched.
- **Side-effectful answers default to `no`** in `copier.yml` (`github_remote_create`,
`github_release_init`, `bunch_add`, `obsidian_project_add`, `run_task_install`).
Leave them off unless the user explicitly asks; only flip them on with confirmation.
- **Run non-interactively** with `--data key=value` for known answers and
`--defaults` for the rest, so runs are reproducible and CI-safe. Use `--trust`
(the template has `_tasks`). Example shape:

```bash
copier copy ~/git/harmon-init ./new-project \
--vcs-ref=HEAD --trust \
--data project_name="My Project" --data project_type=general --defaults
```

- **Validate after every apply.** Re-running `copier` or changing answers can churn
files — confirm the result with the verification step below before committing.

The asked questions live in `~/git/harmon-init/copier.yml` (e.g. `project_name`,
`project_slug`, `project_description`, `github_org`, `project_type`
[general / web-astro / web-app / iac / docs], `include_terraform`, `include_ansible`,
`ci_runner`, `license`, `use_release_please`, `devcontainer`, `git_init`). Read that
file to confirm names/choices/defaults before scaffolding — do not invent answers.

## Standards catalog

The authoritative, itemized list of what "standardized" means — every tool, config
file, Taskfile target, hook, and CI workflow the template provides, and how to check
each — is **`references/standards-catalog.md`**. The audit mode and any manual
retrofit work off that catalog. Treat the generated template output (and that
catalog) as the source of truth, not memory.

## Verification

After applying any mode, run the bundled check:

```bash
assets/verify-applied.sh <target-repo-dir>
```

It confirms the expected files/tooling landed and then runs the repo's own gate
(`task verify` = lint + template/output checks; `task check` for lint only;
`task install:hooks` to wire lefthook). Report what passed and surface any gaps
against `references/standards-catalog.md`. Never bypass hooks (`--no-verify` is
prohibited); commit on a feature branch and open a PR — no direct commits to `main`.
160 changes: 160 additions & 0 deletions ai/skills/repo/standardize-repo/assets/detect-project-type.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#!/usr/bin/env bash
#
# detect-project-type.sh — inspect a repo and print suggested copier --data flags
# for the harmon-init template (https://github.com/evanharmon1/harmon-init).
#
# Usage:
# detect-project-type.sh [TARGET_DIR] # TARGET_DIR defaults to "."
#
# Output:
# stdout — copy-pasteable `--data key=value` flags for `copier copy/update`.
# stderr — a one-line human-readable summary of what was detected.
#
# The emitted flags mirror the questions in harmon-init's copier.yml:
# project_type ∈ {general, web-astro, web-app, iac, docs}
# include_terraform / include_ansible (booleans)
# copier derives use_node/use_python from these, so they are not emitted.
#
# Detection is best-effort. Always review the suggestions before running copier.
# Portable to macOS bash 3.2 (no mapfile, no grep -P, no associative arrays).

set -euo pipefail

target="${1:-.}"

if [ ! -d "$target" ]; then
echo "error: target directory not found: $target" >&2
exit 1
fi

# ── Detection helpers ───────────────────────────────────────────────
# All helpers are silent (exit status only) so callers can branch on them.

pkg_json="$target/package.json"
have_node=false
[ -f "$pkg_json" ] && have_node=true

# pkg_has PATTERN — true if package.json exists and contains PATTERN (case-insensitive).
pkg_has() {
[ -f "$pkg_json" ] || return 1
grep -i -q -e "$1" "$pkg_json"
}

# find_first GLOB... — print the first matching path under target, or nothing.
# Uses find (not bash globbing) so it works without nullglob and recurses.
find_first() {
find "$target" -type f \( "$@" \) 2>/dev/null | head -n 1
}

# Terraform: any *.tf file anywhere, or a terraform/ directory.
have_terraform=false
if [ -d "$target/terraform" ]; then
have_terraform=true
elif [ -n "$(find_first -name '*.tf')" ]; then
have_terraform=true
fi

# Ansible: an ansible/ dir, an ansible.cfg, a pyproject declaring ansible,
# or a *.yml/*.yaml file that looks like a playbook (top-level `hosts:`).
have_ansible=false
ansible_reason=""
if [ -d "$target/ansible" ]; then
have_ansible=true
ansible_reason="ansible/ directory"
elif [ -f "$target/ansible.cfg" ]; then
have_ansible=true
ansible_reason="ansible.cfg"
elif [ -f "$target/pyproject.toml" ] && grep -i -q -e 'ansible' "$target/pyproject.toml"; then
have_ansible=true
ansible_reason="ansible in pyproject.toml"
else
# Scan YAML files for a playbook signature without grep -P or mapfile.
while IFS= read -r yml; do
[ -n "$yml" ] || continue
if grep -E -q '^[[:space:]]*-?[[:space:]]*hosts:[[:space:]]' "$yml"; then
have_ansible=true
ansible_reason="playbook ($yml)"
break
fi
done <<EOF
$(find "$target" -type f \( -name '*.yml' -o -name '*.yaml' \) 2>/dev/null)
EOF
fi

# Python (non-node) project markers.
have_python=false
if [ -f "$target/pyproject.toml" ] || [ -f "$target/requirements.txt" ]; then
have_python=true
fi

# Markdown / docs presence, and whether any "code" file exists at all.
have_markdown=false
if [ -n "$(find_first -name '*.md' -o -name '*.markdown')" ]; then
have_markdown=true
fi

# A small set of source extensions that would disqualify a "docs-only" repo.
have_code=false
if [ -n "$(
find_first \
-name '*.ts' -o -name '*.tsx' -o -name '*.js' -o -name '*.jsx' \
-o -name '*.py' -o -name '*.go' -o -name '*.rs' -o -name '*.rb' \
-o -name '*.sh' -o -name '*.tf' -o -name '*.java' -o -name '*.c' \
-o -name '*.cpp' -o -name '*.cs' -o -name '*.php'
)" ]; then
have_code=true
fi

# ── Decide project_type ─────────────────────────────────────────────
# Priority: web-astro > web-app > iac > docs > general. Terraform/Ansible
# also force iac when no web framework is present.

project_type="general"
type_reason="no stronger signal"

if "$have_node" && pkg_has 'astro'; then
project_type="web-astro"
type_reason="astro in package.json"
elif "$have_node" && { pkg_has 'react' || pkg_has '@tanstack'; }; then
project_type="web-app"
type_reason="react/@tanstack in package.json"
elif "$have_ansible" || "$have_terraform"; then
project_type="iac"
if "$have_ansible" && "$have_terraform"; then
type_reason="terraform + ansible"
elif "$have_ansible"; then
type_reason="ansible ($ansible_reason)"
else
type_reason="terraform files"
fi
elif "$have_python" && ! "$have_node"; then
# A Python project that isn't IaC and isn't a JS app: closest fit is general.
project_type="general"
type_reason="python project (pyproject/requirements, no node)"
elif "$have_markdown" && ! "$have_code"; then
project_type="docs"
type_reason="markdown only, no source files"
fi

# ── Emit copier --data flags ────────────────────────────────────────

printf -- '--data project_type=%s\n' "$project_type"

if "$have_terraform"; then
printf -- '--data include_terraform=true\n'
fi

if "$have_ansible"; then
printf -- '--data include_ansible=true\n'
fi

# ── Human summary (stderr) ──────────────────────────────────────────

summary="detected project_type=$project_type ($type_reason)"
if "$have_terraform"; then
summary="$summary; include_terraform=true"
fi
if "$have_ansible"; then
summary="$summary; include_ansible=true"
fi
echo "$summary" >&2
Loading
Loading