diff --git a/README.md b/README.md index 9e91cce..482d067 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/ai/skills/repo/standardize-repo/SKILL.md b/ai/skills/repo/standardize-repo/SKILL.md new file mode 100644 index 0000000..daccda4 --- /dev/null +++ b/ai/skills/repo/standardize-repo/SKILL.md @@ -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 +``` + +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`. diff --git a/ai/skills/repo/standardize-repo/assets/detect-project-type.sh b/ai/skills/repo/standardize-repo/assets/detect-project-type.sh new file mode 100755 index 0000000..54f19d3 --- /dev/null +++ b/ai/skills/repo/standardize-repo/assets/detect-project-type.sh @@ -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 </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 diff --git a/ai/skills/repo/standardize-repo/assets/verify-applied.sh b/ai/skills/repo/standardize-repo/assets/verify-applied.sh new file mode 100755 index 0000000..4dd52ba --- /dev/null +++ b/ai/skills/repo/standardize-repo/assets/verify-applied.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# +# verify-applied.sh — validate a repo AFTER harmon-init conventions were applied. +# +# Usage: +# verify-applied.sh [TARGET_DIR] # TARGET_DIR defaults to "." +# +# Mirrors the validation philosophy of harmon-init's scripts/test-template.sh, +# but runs against an ALREADY-RENDERED, real repo (the result of `copier copy` +# / `copier update`), not a throwaway copier render. So it: +# - delegates the heavy linting to the repo's own gate (`task verify`) instead +# of re-implementing every linter, and +# - spot-checks the structural invariants the template guarantees +# (AGENTS.md canonical + agent-instruction symlinks, a parseable Taskfile, +# no unrendered jinja markers, no leaked secrets). +# +# All checks accumulate; the script exits non-zero if ANY check fails, so it is +# safe to run as a post-apply gate in CI or locally. +# +# Portable to macOS bash 3.2 (no mapfile, no grep -P, no associative arrays). + +set -euo pipefail + +target="${1:-.}" + +if [ ! -d "$target" ]; then + echo "FAIL: target directory not found: $target" >&2 + exit 1 +fi + +cd "$target" + +have() { command -v "$1" >/dev/null 2>&1; } + +fail=0 +err() { + echo "FAIL: $*" >&2 + fail=1 +} + +echo "Verifying applied conventions in: $(pwd)" + +# ── 1. The repo's own gate: `task verify` (lint + output checks) ───── +# This is the authoritative check — it runs whatever lint/test targets the +# generated Taskfile defines. We only orchestrate the structural spot-checks +# below; we do NOT duplicate the linters here. +if [ -f Taskfile.yml ] || [ -f Taskfile.yaml ]; then + if have task; then + if ! task verify; then + err "'task verify' failed" + fi + else + echo "WARN: 'task' (go-task) not installed — skipping 'task verify' gate" + fi +else + echo "WARN: no Taskfile.yml — repo may not have been standardized yet" +fi + +# ── 2. AGENTS.md is canonical; agent-instruction files symlink to it ─ +# copier.yml sets _preserve_symlinks: true so CLAUDE.md / GEMINI.md / +# .github/copilot-instructions.md stay as links pointing at AGENTS.md +# (copilot's canonical path is one dir down, so it links to ../AGENTS.md). +if [ ! -e AGENTS.md ]; then + err "AGENTS.md missing" +elif [ -L AGENTS.md ] || [ ! -f AGENTS.md ]; then + err "AGENTS.md should be a regular file, not a symlink or directory" +fi + +for link in CLAUDE.md GEMINI.md; do + if [ ! -L "$link" ]; then + err "$link should be a symlink to AGENTS.md" + elif [ "$(readlink "$link")" != "AGENTS.md" ]; then + err "$link should resolve to AGENTS.md (found: $(readlink "$link"))" + fi +done + +# copilot's instructions file is optional, but if present it must link upward. +copilot=".github/copilot-instructions.md" +if [ -e "$copilot" ] || [ -L "$copilot" ]; then + if [ ! -L "$copilot" ]; then + err "$copilot should be a symlink to ../AGENTS.md" + elif [ "$(readlink "$copilot")" != "../AGENTS.md" ]; then + err "$copilot should resolve to ../AGENTS.md (found: $(readlink "$copilot"))" + fi +fi + +# ── 3. The generated Taskfile actually parses ─────────────────────── +# `task verify` above would catch this too, but a broken Taskfile makes that +# step error out ambiguously; this gives a precise message. +if { [ -f Taskfile.yml ] || [ -f Taskfile.yaml ]; } && have task; then + if ! task --list-all >/dev/null 2>&1; then + err "Taskfile does not parse ('task --list-all' failed)" + fi +fi + +# ── 4. No unrendered template markers leaked into the repo ────────── +# harmon-init uses CUSTOM jinja delimiters ([[ var ]], [% block %]). Legitimate +# look-alikes must NOT trip this: go-task uses {{.VAR}} (dot, no space), GitHub +# Actions uses ${{ }}, bash uses [[ -n "$x" ]] / array[idx], and terminfo uses +# \E[%p1%d — none of which have the "" +# shape we match. We anchor variable markers on the copier answer-variable name +# stems (kept in sync with copier.yml; every question variable must be covered +# by one stem) so a real leak ([[ git_init ]], {{ author_full_name }}) is caught +# while bash bare-word tests ([[ true ]]) are not. Block markers anchor on the +# jinja keyword set, including the raw/endraw the template actually emits and the +# [%- whitespace-control form used in LICENSE.jinja. +varpfx='project_|author_|github_|organization|repo_url|ci_runner|include_|use_|devcontainer|git_init|bunch_add|obsidian_|run_task_install|projects_directory|bunches_directory|license|current_|country|state' +blockkw='if|for|set|else|elif|endif|endfor|endset|raw|endraw|macro|endmacro|block|endblock|include|extends|with|endwith|filter|endfilter' +leaks=$(grep -rIlE \ + "\[\[-? ($varpfx)|\{\{-? ($varpfx)|\[%-? ($blockkw) " \ + --exclude-dir=.git --exclude-dir=node_modules . 2>/dev/null || true) +if [ -n "$leaks" ]; then + err "unrendered template markers found in:" + # Print one path per line for readability; indented so it groups under the FAIL. + echo "$leaks" | sed 's/^/ /' >&2 +fi + +# ── 5. No secrets committed/sitting in the tree (gitleaks) ────────── +# Matches test-template.sh: gitleaks is best-effort locally, but if it is +# installed a finding is a hard failure. +if have gitleaks; then + if ! gitleaks detect --no-banner --redact --source .; then + err "gitleaks reported findings" + fi +else + echo "WARN: gitleaks not installed — skipping secrets scan" +fi + +# ── Result ────────────────────────────────────────────────────────── +if [ "$fail" -ne 0 ]; then + echo "verify-applied: FAILED" >&2 + exit 1 +fi +echo "verify-applied: PASS" diff --git a/ai/skills/repo/standardize-repo/references/copier-gotchas.md b/ai/skills/repo/standardize-repo/references/copier-gotchas.md new file mode 100644 index 0000000..2eb42c4 --- /dev/null +++ b/ai/skills/repo/standardize-repo/references/copier-gotchas.md @@ -0,0 +1,187 @@ +# Copier gotchas (load-bearing mechanics) + +The `harmon-init` template (`/Users/evan/git/harmon-init`) sets custom jinja +delimiters and several copier behaviors that, if ignored, produce *silently wrong* +renders — no error, just missing or stale output. Respect every rule below when +authoring `template/**`, editing `copier.yml`, or running `copier copy` / `copier +update` against a local checkout of the template. + +Authority: these are derived from `copier.yml` (`_envops`, `_preserve_symlinks`, +`_tasks`, `_min_copier_version`), `scripts/test-template.sh`, `.gitignore`, and the +"Critical Copier Gotchas" section of `AGENTS.md`. `_min_copier_version` is `9.4.0`. + +--- + +## 1. `--vcs-ref=HEAD` is load-bearing for local-path templates + +**Symptom:** You copy from a local path, then your uncommitted (or committed-but- +untagged) template edits don't appear in the output — copier renders an *old* repo +state with zero warning. + +**Why:** When the template source is a git repo, `copier copy ` renders +the **latest git tag**, not your working tree. All uncommitted AND committed-but- +untagged work is silently ignored. + +**Rule:** Always pass `--vcs-ref=HEAD` when rendering from a local checkout for +testing. With it, copier auto-includes dirty **and** untracked changes via a +throwaway `wip` commit in a temporary clone (you'll see a `DirtyLocalWarning`). Your +real working tree is never touched. `scripts/test-template.sh` always passes +`--vcs-ref=HEAD` — mirror that for any manual render of work-in-progress. + +```bash +copier copy --trust --vcs-ref=HEAD /path/to/harmon-init /path/to/dest +``` + +--- + +## 2. Custom jinja delimiters — `[[ ]]`, `[% %]`, `[# #]` + +**Symptom:** Standard `{{ }}` / `{% %}` syntax in a `.jinja` file passes through +unrendered (because GitHub Actions `${{ }}`, go-task `{{.VAR}}`, and lefthook +`{staged_files}` are meant to survive verbatim), or a stray blank line / eaten +newline corrupts the rendered file. + +**Why:** `copier.yml`'s `_envops` remaps the delimiters specifically so CI/task/hook +syntax needs zero escaping: + +| Construct | Delimiter | +|-----------|-----------| +| variable | `[[ var ]]` | +| block | `[% if x %]` … `[% endif %]` | +| comment | `[# comment #]` | + +`trim_blocks: true` and `lstrip_blocks: true` are also set, so block tags on their +own line don't leave blank lines. + +**Rules:** + +- Use `[[ ]]` / `[% %]` / `[# #]` for *all* templating. Leave `${{ }}`, `{{.VAR}}`, + `{staged_files}` untouched — they render verbatim. +- **Inside `.sh.jinja` (and any shell), use POSIX single-bracket tests `[ ... ]`, + not bash `[[ ... ]]`.** A bash `[[` opens a jinja variable delimiter and breaks the + render. (See `template/[% if devcontainer %].devcontainer[% endif %]/post-create.sh.jinja`, + which uses `if [ -d ... ]`.) This also keeps scripts portable to macOS bash 3.2. +- **An inline `[% endif %]` at end-of-line eats the following newline.** When the + `endif` sits at the end of a content line (not on its own line), write + `[% endif +%]` to preserve the newline. Real examples: + `template/renovate.json.jinja:106` (`}[% endif +%]`) and + `template/.github/workflows/build.yml.jinja:84` + (`- name: Lint[% if use_node %] + typecheck[% endif +%]`). A standalone + `[% endif %]` on its own line is fine (see `template/.gitignore.jinja`). + +--- + +## 3. `_preserve_symlinks: true` keeps the AGENTS.md symlinks + +**Symptom:** Without it, copier dereferences symlinks and the generated project gets +three duplicate copies of the instructions file instead of links. + +**Why:** `copier.yml` sets `_preserve_symlinks: true` so `CLAUDE.md`, `GEMINI.md`, +and `.github/copilot-instructions.md` stay **symlinks** to the canonical `AGENTS.md` +(copilot's link targets `../AGENTS.md`). + +**Rule:** Edit only `AGENTS.md`; never edit the three symlinks. After any render, +`scripts/test-template.sh` asserts each is a symlink to the right target — keep it +that way when authoring template files. + +--- + +## 4. Side-effectful answers default to `no` (CI-safe `--defaults`) + +**Symptom:** `copier copy --defaults` in CI would otherwise create a GitHub repo, cut +a release, or move files into iCloud / an Obsidian vault. + +**Why & rule:** These questions in `copier.yml` all default to **no** so +`copier copy --defaults` is side-effect-free: + +- `bunch_add` (macOS-only; moves a file to iCloud) +- `github_remote_create` (`gh repo create --private --push`) +- `github_release_init` (`task release:init`) +- `run_task_install` (`task install` — brew bundle + git hooks) +- (`obsidian_project_add` is likewise default `no`) + +When adding any new question whose `_tasks` command has an external side effect, +**default it to `no`**. The only exception is `git_init` (default `yes`) because it +touches only the new project directory. + +--- + +## 5. Unanchored `.meta` (or a global `~/.gitignore`) silently drops `template/.meta` + +**Symptom:** A dirty render is missing the Bunch / Obsidian notes under +`template/.meta/` — they never make it into the output, with no error. + +**Why:** Copier's dirty-tree path clones the template with `git clone --no-checkout` +(empty index) and then runs `git add -A` to build the throwaway `wip` commit. Because +the index is empty, gitignore rules apply **even to already-tracked files**. An +**unanchored** `.meta` pattern (in this repo's `.gitignore` or a developer's global +`~/.gitignore`) therefore matches `template/.meta` and excludes it from the wip +commit — so it's absent from the render. + +**Rule:** Anchor ignore patterns to the repo root and re-include the template's copy +with negations. This repo's `.gitignore` does exactly that: + +```gitignore +/.meta # anchored — was matching template/.meta unanchored +!template/.meta/ +!template/.meta/** +``` + +If a render is missing `template/.meta` content, suspect a global `~/.gitignore` with +an unanchored `.meta` and add the negations above. + +--- + +## 6. Conditionally-named files aren't compiled unless an answer makes the name non-empty + +**Symptom:** A syntax error inside a `[% if ... %]`-named file ships silently — +nothing flags it — because no answer profile ever renders that file. + +**Why:** Copier skips any file whose **rendered name is empty**. Files named with a +condition (e.g. `template/[% if use_release_please %]release-please-config.json[% endif %].jinja`, +`template/[% if include_terraform %]terraform[% endif %]/`, +`template/.meta/[% if bunch_add %]Code Project - [[ project_name ]].bunch[% endif %].jinja`, +`template/.github/workflows/[% if github_org != author_git_provider_username %]project-automation.yml[% endif %].jinja`) +are never even compiled by jinja until some answer set makes that name non-empty. + +**Rule:** Every `[% if ... %]`-named file/directory must be covered by at least one +profile in `scripts/test-template.sh` (`minimal | web | iac | full | meta`), or its +jinja/syntax errors never surface. When you add a new conditionally-named file, +ensure an existing profile turns its condition on — or extend the profiles — and run +`task test:template:all`. + +--- + +## 7. The initial scaffold commit must run before remote-create / release-init + +**Symptom:** `gh repo create --push` or `task release:init` fails because `HEAD` +doesn't exist yet. + +**Why:** The `_tasks` in `copier.yml` are ordered so that, when `git_init` is on, +`git init -b main` is immediately followed by: + +```yaml +git add -A && git commit -m "chore: initial scaffold from harmon-init" +``` + +Both the `github_remote_create` task (`gh repo create ... --push`) and the +`github_release_init` task (`task release:init`) require `HEAD` to exist. This commit +also runs **before** `task install`, so lefthook hooks aren't installed yet and +nothing intercepts it. + +**Rule:** Preserve this `_tasks` ordering — `git init` → initial scaffold commit → +remote create / release init / task install. `scripts/test-template.sh` asserts the +rendered repo has a commit (`git rev-parse HEAD`) whenever `_tasks` ran; don't add a +remote/release task ahead of the scaffold commit. + +--- + +## Quick checklist when touching the template + +- Rendering local WIP to test? → `--vcs-ref=HEAD`. +- New templating? → `[[ ]]` / `[% %]` / `[# #]`; POSIX `[ ]` in shell; `[% endif +%]` + inline. +- New side-effect question? → default `no`. +- New conditionally-named file? → cover it with a `test-template.sh` profile. +- New ignore pattern? → anchor to `/` and negate `template/` copies. +- After any `copier.yml` / `template/**` change → `task test:template:all` must pass. diff --git a/ai/skills/repo/standardize-repo/references/mode-adopt-existing.md b/ai/skills/repo/standardize-repo/references/mode-adopt-existing.md new file mode 100644 index 0000000..3116015 --- /dev/null +++ b/ai/skills/repo/standardize-repo/references/mode-adopt-existing.md @@ -0,0 +1,258 @@ +# Mode: Adopt harmon-init in an EXISTING repo + +Apply (or re-apply) the [harmon-init](https://github.com/evanharmon1/harmon-init) +Copier template to a repo that **already has app code**. The hard rule: this is a +*standardization* pass over a living codebase, not a fresh scaffold — +**never blind-clobber existing app code.** Read each conflict, prefer merging, and +work on a feature branch the whole time. + +For Copier mechanics (custom `[[ ]]`/`[% %]` jinja delimiters, the load-bearing +`--vcs-ref=HEAD`, `--trust`, side-effect answers) see +[`copier-gotchas.md`](./copier-gotchas.md). For the GitHub-side wiring after the +files land (branch ruleset import, Renovate/CodeRabbit apps, Actions secrets, etc.) +see [`post-generation-checklist.md`](./post-generation-checklist.md). + +--- + +## 0. Always branch first — never main + +Direct commits to `main` are forbidden by the conventions this template installs +(lefthook `guard:no-commit-to-main` + branch ruleset). Do all adoption work on a +feature branch from a clean tree: + +```bash +cd ~/git/ +git switch main && git pull +git status --porcelain # MUST be empty; stash or commit first +git switch -c chore/adopt-harmon-init +``` + +A clean tree is also what makes conflict review legible: after the copier run, +`git diff` shows exactly what the template wants to change, file by file. + +--- + +## 1. Detect the project type → turn it into `--data` + +Copier's first decision is `project_type`, which drives the Taskfile, CI jobs, and +devcontainer tooling. Don't guess it — run the detector against the target repo: + +```bash +~/git/harmon-devkit/ai/skills/repo/standardize-repo/assets/detect-project-type.sh . +``` + +It inspects the repo and prints the matching `project_type`, one of the five values +defined in `copier.yml`: + +| `project_type` | When | +| --- | --- | +| `general` | default — no framework/IaC signal | +| `web-astro` | Astro marketing/static site | +| `web-app` | TanStack/React app | +| `iac` | Terraform/Ansible infrastructure | +| `docs` | documentation / Obsidian vault | + +Feed the result straight into the copier `--data` flags. The questions you'll +normally set explicitly when adopting an existing repo (the rest fall back to +sensible `copier.yml` defaults): + +```bash +PROJECT_TYPE="$(~/git/harmon-devkit/ai/skills/repo/standardize-repo/assets/detect-project-type.sh .)" + +--data project_type="$PROJECT_TYPE" \ +--data project_name="" \ +--data project_slug="$(basename "$(pwd)")" \ +--data project_description="" \ +--data github_org="" +``` + +Defaults worth knowing so you only override what's wrong (from `copier.yml`): + +- `project_slug` defaults to `project_name` lowercased with spaces → `-`. +- `include_terraform` / `include_ansible` default to **true when + `project_type == 'iac'`**, false otherwise. Override to `true`/`false` if a + non-iac repo nonetheless has (or wants) a `terraform/` or `ansible/` skeleton. +- `ci_runner` defaults to `ubuntu-latest` (alt: `self-hosted`). +- `license` defaults to `mit` (alt: `private`). +- `use_release_please` and `devcontainer` default to **yes**. +- **All side-effect answers must stay `no`** when adopting: `git_init`, + `github_remote_create`, `github_release_init`, `bunch_add`, + `obsidian_project_add`, `run_task_install`. The repo already exists and has a + remote/history — let those run by hand later, not as a copier `_task`. Pass + `--defaults` (with the `--data` overrides above) to lock the rest down + non-interactively, or answer interactively and explicitly decline each one. + +--- + +## 2. Two adoption paths + +### Path A — repo was generated from this template (`copier update`) + +If a `.copier-answers.yml` exists at the repo root, the repo is already linked to +the template. Update in place; copier performs a three-way merge between the old +template output, the new template output, and your current files: + +```bash +ls .copier-answers.yml # present → use update +copier update --trust --vcs-ref=HEAD +``` + +`copier update` takes no source argument — it reuses the `_src_path` recorded in +`.copier-answers.yml`. When that `_src_path` is a **local** harmon-init checkout +(which it is for repos adopted via Path B's `copier copy ~/git/harmon-init`), +pass `--vcs-ref=HEAD` so update renders your working tree. Omitting it renders the +latest **local git tag** of that checkout — silently dropping any +committed-but-untagged + uncommitted template work, the exact trap described in +`copier-gotchas.md`. Use `--vcs-ref=HEAD` unless you deliberately want the last +tagged release. Override stale answers with `--data key=value` as needed (e.g. a +changed `github_org`). + +> v2-generated repos are the exception: per the harmon-init README, v3 was a +> breaking redesign (new question set, jinja delimiters, lefthook+gitleaks, manual +> releases, dual-profile devcontainer, canonical AGENTS.md). **Re-template them via +> Path B and reconcile** rather than `copier update`. + +### Path B — adopt fresh (repo was NOT generated from the template) + +No `.copier-answers.yml`. Copy the template *over* the existing repo. Copier will +write `.copier-answers.yml` so future runs can use `copier update`: + +```bash +ls .copier-answers.yml # absent → adopt fresh +copier copy --trust ~/git/harmon-init . --vcs-ref=HEAD \ + --data project_type="$PROJECT_TYPE" \ + --data project_name="" \ + --data project_slug="$(basename "$(pwd)")" \ + --data github_org="" \ + # ...remaining --data / --defaults, side-effect answers = no +``` + +`--vcs-ref=HEAD` is **mandatory** here when `~/git/harmon-init` is a local path: +without it copier silently renders the latest git tag and ignores +committed-but-untagged + uncommitted template work. + +--- + +## 3. Overwrite / conflict handling — review every conflict + +When copier touches a file that already exists with different content it prompts +per file. The cardinal rule: **never blind-clobber existing app code; prefer +merging.** + +- **Conflict prompt (`copier copy` over existing files):** copier asks to + overwrite each differing file `(y/n)`. + - **App/source code, business logic, real READMEs, existing docs with content:** + answer **n** (keep yours). Reconcile by hand afterward. + - **Pure tooling/config the template owns** (`Taskfile.yml`, `lefthook.yml`, + `.github/workflows/*`, `.editorconfig`, `.gitleaks.toml`, `renovate.json`, + `commitlint.config.mjs`, devcontainer): generally accept the template version, + then re-apply any genuine local customizations on top. +- **`copier update` (Path A):** conflicts surface as `.rej` files / inline merge + markers. Resolve like a git merge — `grep -rn '^<<<<<<<\|^=======\|^>>>>>>>' .` + and `find . -name '*.rej'`, then edit each so both the template's intent and the + repo's real content survive. +- After resolving, **read the full diff before staging**: + `git add -A -N && git diff` — confirm nothing under app/source paths was + silently overwritten, and no copier variable leaked (search for the literal + `[[`, `[%`, or unresolved `TODO: project_description`). + +When in genuine doubt about a specific file, keep the existing version and leave a +`TODO:` note rather than discarding working code. + +--- + +## 4. Reconciliation steps specific to existing repos + +These are the recurring drifts harmon-init exists to fix (source: +`harmon-init/docs/sourceRepoFollowUps.md`). Walk each one after the copier run: + +1. **AGENTS.md is canonical; everything else symlinks to it.** The template ships + `AGENTS.md` as the real file with `CLAUDE.md`, `GEMINI.md`, and + `.github/copilot-instructions.md` as symlinks → `AGENTS.md`. Existing repos + frequently have it backwards (e.g. `CLAUDE.md` is the real file). Flip it: + + ```bash + # make AGENTS.md the single source of truth, then symlink the rest to it + ln -sf AGENTS.md CLAUDE.md + ln -sf AGENTS.md GEMINI.md + ln -sf ../AGENTS.md .github/copilot-instructions.md # note: ../ from .github/ + ``` + + Merge any unique guidance from the old real file into `AGENTS.md` *before* + replacing it with a symlink. Then confirm the tool excludes are in place so + linters don't choke on the symlinks: `lefthook.yml`'s prettier hook must + `exclude` `CLAUDE.md`, `GEMINI.md`, and `.github/copilot-instructions.md` + (these are explicit excludes in the template's `lefthook.yml`). + Verify: `ls -l CLAUDE.md GEMINI.md .github/copilot-instructions.md` should all + show `-> AGENTS.md` / `-> ../AGENTS.md`. + +2. **Align the docs layout** to the template's tree. Specs live at **root + `specs/`** (move any `docs/specs/` → `specs/`); tests at **root `tests/`**. + Ensure these exist (the template seeds them): `docs/README.md`, + `docs/glossary.md`, `docs/conventions.md`, `docs/guides/` (incl. + `onboarding.md`), `docs/architecture/` (incl. `tests.md`, `security.md`, + `ci-cd.md`), `docs/product/` (incl. `roadmap.md`, `vision.md`), + `docs/decisions/` (ADRs, with the seed `0001-record-architecture-decisions.md`), + `docs/runbooks/` (**plural**), and `docs/CHECKLIST.md`. Don't delete existing + docs with real content — fold them into the standard locations. + +3. **Leave YAML extensions alone.** Do not rename `.yaml`↔`.yml`. Each tool + keeps its own conventional extension (`Taskfile.yml`, `.coderabbit.yaml`, + GitHub Actions accepts either) — homogenizing extensions across the repo is + not a goal. + + (Note `.coderabbit.yaml` is intentionally `.yaml` — leave it.) After renaming, + update any branch-ruleset required-check contexts that referenced the old job + names (see `post-generation-checklist.md`). + +4. **Add `# renovate:` annotations to tool pins.** Inline tool-version pins in + workflows must carry a renovate annotation so updates are automated and pins + stop diverging across repos. Match the template's format exactly — the comment + sits on the line directly above the pinned `VERSION=` assignment. Example from + the template's `build.yml` gitleaks install: + + ```bash + # renovate: datasource=github-releases depName=gitleaks/gitleaks extractVersion=^v?(?.+)$ + GITLEAKS_VERSION=8.24.3 + ``` + + Do the same for any other un-annotated pins you find (go-task, shellcheck, + shfmt, actionlint, node versions). Pin third-party GitHub Actions by commit SHA + with a trailing version comment. + +5. **Other recurring fixes** (apply if present): consolidate duplicate Claude + workflows (`claude-*-max.yml` → the base `claude-plan/implement/review.yml`); + add `codeql.yml` if missing and the repo uses node/python; drop any + bump-on-merge `release.yml` in favor of release-please; de-bloat a legacy + `Brewfile`; make scripts portable to macOS bash 3.2 (no `mapfile`, no + `grep -P`). + +--- + +## 5. Verify, then commit on the branch + +Run the same gate the template installs, then the skill's applied-state check: + +```bash +task install # one-time: brew deps + lefthook hooks (safe to run now) +task verify # the merge gate: lint → (build) → validate +~/git/harmon-devkit/ai/skills/repo/standardize-repo/assets/verify-applied.sh . +``` + +`task verify` runs `check` (all linters[, typecheck], parallel), an optional +`build` for node projects, then `validate`. `verify-applied.sh` confirms the +adoption actually took (canonical AGENTS.md + symlinks, docs layout, `.yml` +extensions, renovate annotations, no leaked copier vars). Fix everything they flag. + +Then stage, review the **full** diff one more time, and commit on the feature +branch with a Conventional Commit message (types enforced by commitlint): + +```bash +git add -A +git diff --cached # final read — no clobbered app code, no leaked [[ ]]/[% %] +git commit -m "chore: adopt harmon-init conventions" +``` + +Never bypass hooks with `--no-verify`. Open a PR (code-owner review + `verify` and +`security` status checks are required) — do not merge to `main` directly. Finish +the GitHub-side wiring per [`post-generation-checklist.md`](./post-generation-checklist.md). diff --git a/ai/skills/repo/standardize-repo/references/mode-audit.md b/ai/skills/repo/standardize-repo/references/mode-audit.md new file mode 100644 index 0000000..eccf62a --- /dev/null +++ b/ai/skills/repo/standardize-repo/references/mode-audit.md @@ -0,0 +1,288 @@ +# Mode: Audit + +Audit an existing repo against the harmon-init conventions, report the drift, and +reconcile it. Use this mode when the goal is to **assess and remediate** a repo +(generated from the template or not) rather than scaffold a new one. + +Source of truth: the harmon-init template at `~/git/harmon-init` (the `template/` +subdirectory is what gets generated; the repo root "dogfoods" the same conventions +for maintaining the template). The per-area checklist lives in +[`references/standards-catalog.md`](./standards-catalog.md) — that catalog is the +authoritative list of what "standardized" means; this file is the **procedure** for +walking it against a target and fixing gaps. + +--- + +## 1. Running the audit + +Work the target repo area-by-area, in the same order the catalog +([`references/standards-catalog.md`](./standards-catalog.md)) presents them. For +each area: read the catalog entry, inspect the corresponding files in the target, +and compare against the template. Do not assert a gap from memory — verify it. + +### Setup + +```bash +TARGET=/abs/path/to/target-repo # the repo being audited +TEMPLATE=~/git/harmon-init # source of truth +``` + +1. **Establish provenance.** Check whether the repo was generated from the + template: look for `.copier-answers.yml` at the target root. + - Present → it can be reconciled with `copier update --trust` (it records + `_commit` / `_src_path`). Note the recorded commit; drift is "template moved + ahead since then." + - Absent → it was never templated (or was adopted by a raw `copier copy`). + Reconciling the templated bits means a fresh adopt: + `copier copy --trust ~/git/harmon-init . --vcs-ref=HEAD` (see §4). Treat + every catalog area as hand-verifiable rather than diff-against-answers. + +2. **Walk each catalog area against the target.** For every area in + `standards-catalog.md`, do the three-way check: + - What the catalog requires. + - What the template actually ships (read the real file under + `$TEMPLATE/template/...`, or the dogfooded root copy — do not guess paths, + task names, or tool versions). + - What the target currently has. + + Useful evidence-gathering commands (read-only; never mutate the target during + the audit phase): + + ```bash + # AGENTS.md symlink direction (see drift class A) + ls -la "$TARGET" | grep -iE 'AGENTS|CLAUDE|GEMINI' + ls -la "$TARGET/.github/copilot-instructions.md" 2>/dev/null + + # Docs layout (drift class B) + find "$TARGET/docs" -maxdepth 2 2>/dev/null | sort + ls -d "$TARGET/specs" "$TARGET/tests" 2>/dev/null + + # Workflow inventory + extensions (drift classes E, F, G, H) + ls -la "$TARGET/.github/workflows/" + + # Renovate/version-pin annotations (drift class C) + grep -rnE 'GITLEAKS_VERSION|setup-task|# renovate:' "$TARGET/.github/workflows/" 2>/dev/null + + # Required status checks / merge_queue in the ruleset (drift class D) + find "$TARGET/.github" -iname '*ruleset*' + ``` + +3. **If `task` is available in the target**, run the standard gates as a live + signal of conformance (they are themselves part of the standard): + + ```bash + ( cd "$TARGET" && task verify ) # check (lint) -> test:template + ( cd "$TARGET" && task security ) # secrets (gitleaks) + audit + ``` + + A repo missing `task verify` / `task security`, or whose tasks don't delegate + lint to a Taskfile target, is itself a finding (the standard is "every hook / + CI job delegates to a `task` target"). + +4. **Record each comparison as a gap-report line** (format in §2). Skip nothing: + an area that fully matches is recorded as "OK" so the report is a complete + ledger, not just a problem list. + +--- + +## 2. Gap-report format + +Produce one report, grouped by catalog area, in the catalog's order. Each finding +carries a **severity** and a **concrete fix** (the exact file/edit/command, not +"fix the workflow"). Severities: + +- **blocker** — breaks a required CI gate, security control, or the branch + ruleset; or makes `task verify` / `task security` fail. Must be fixed before the + repo is "standardized." +- **should** — a real convention divergence with no immediate breakage (stale job + names that still pass, missing docs scaffold, un-annotated version pins). Fix in + the same pass unless explicitly deferred. +- **nice** — cosmetic / low-risk normalization (naming consistency, legacy + graveyard cleanup). + +### Template + +```markdown +## + +- [blocker] + - Evidence: + - Fix: +- [should] + - Evidence: ... + - Fix: ... +- OK — (so the ledger is complete) +``` + +End the report with a short **Reconciliation plan**: the ordered list of fixes, +which ones a `copier` re-template will resolve automatically vs. which need a +hand-edit, and the verification command set from §4. + +--- + +## 3. Common drift classes to check + +These are the recurring divergences observed when porting real repos onto the +template (seeded from `~/git/harmon-init/docs/sourceRepoFollowUps.md`). Treat the +list as a checklist of likely findings — confirm each against the target before +reporting, and map each to its catalog area. + +**A. AGENTS.md symlink direction.** Canonical layout: `AGENTS.md` is the real +file; `CLAUDE.md`, `GEMINI.md`, **and `.github/copilot-instructions.md`** are +symlinks to it (copilot's default path is `.github/copilot-instructions.md` → +`../AGENTS.md`). Old repos invert this (`CLAUDE.md` real, the rest symlinked). +Fix: make `AGENTS.md` canonical, repoint the three symlinks, and flip the +prettier/lefthook symlink excludes (the template lefthook `pre-commit` prettier +step excludes `CLAUDE.md`, `GEMINI.md`, `.github/copilot-instructions.md` because +prettier errors when handed a symlink). Severity: **should** (blocker if a hook +fails on the symlink). `_preserve_symlinks: true` in `copier.yml` keeps these as +symlinks on re-template. + +**B. Docs layout drift.** Standard docs tree (from `template/docs/`): +`docs/README.md`, `docs/architecture/{README,ci-cd,branch-protection,security,tests}.md`, +`docs/glossary.md`, `docs/conventions.md`, `docs/guides/{README,onboarding,deploying,troubleshooting}.md`, +`docs/product/{README,vision,domain,roadmap}.md`, `docs/decisions/` (ADRs, seeded +with `0001-record-architecture-decisions.md` + `README.md`), and +`docs/runbooks/` — **plural `runbooks/`** (matches harmon-infra; old repos use +singular `runbook`). Also: `specs/` and `tests/` belong at **repo root**, not +under `docs/` (old repos nest `docs/specs/`). Common misses: no `guides/`, no +`product/`, no ADR dir, no `architecture/tests.md` or `glossary.md`. Fix: move +`docs/specs/` → `specs/`, rename `runbook/` → `runbooks/`, and add the missing +scaffold (a re-template fills these in; rename/move are hand-edits because copier +won't delete the old paths). Severity: **should**. + +**C. Version pins lacking renovate annotations.** `gitleaks` and `arduino/setup-task` +must be pinned with a managed-version annotation so Renovate bumps them. Template +standard (verify against the real workflow — versions move): +- gitleaks via a `GITLEAKS_VERSION=...` env line preceded by + `# renovate: datasource=github-releases depName=gitleaks/gitleaks extractVersion=^v?(?.+)$` +- go-task via `arduino/setup-task@ # v2.0.0` with `version:` preceded by + `# renovate: datasource=github-releases depName=go-task/task extractVersion=^v(?.+)$` + +Old repos pin divergent, un-annotated versions (e.g. gitleaks 8.24.3 vs 8.21.2; +setup-task 3.51.1 vs 3.49.x). Fix: copy the annotated pin blocks from the current +template `build.yml`; do **not** hardcode a version from this doc — read the live +value from `$TEMPLATE/.github/workflows/build.yml`. Severity: **should** (blocker +if an unpinned/missing tool breaks the `security` job). + +**D. Stale branch ruleset / old job names / missing `merge_queue`.** Canonical +ruleset (`template/.github/Branch Protection Ruleset - Protect Main.json`) requires +exactly two status-check contexts — **`verify`** and **`security`** — plus a +**`merge_queue`** rule. Old repos reference retired job names (`secrets`, +`validate`, `build-homepage`) and lack the merge-queue rule. Note this drift can +also live in *prose*: even the harmon-init root `docs/architecture/branch-protection.md` +still narrates the old `secrets`/`validate`/`build-homepage` contexts while the +shipped ruleset JSON is already `verify`+`security` — so check the JSON, not just +the doc. Relatedly, ambiguous `verify` contexts: if both `build.yml` and the +devcontainer workflow define a job literally named `verify`, either can satisfy +the required check — the template renames the devcontainer job to +**`devcontainer-verify`**. Fix: re-import the ruleset +(`gh api repos///rulesets --method POST --input ".json"`), +rename the devcontainer job, and align CI job names to `verify` + `security`. +Severity: **blocker** (wrong contexts mean the gate is unenforced or unsatisfiable). + +**E. YAML file extensions — NOT drift; do not flag.** `.yml` vs `.yaml` is left +to each tool's own convention (`Taskfile.yml`, `.coderabbit.yaml`, GitHub Actions +accepts either). Never rename a tool's file to homogenize extensions across the +repo. Severity: **none** (listed only so audits don't wrongly raise it). + +**F. Duplicate / `-max` Claude workflows to consolidate.** Standard set is exactly +three: `claude-plan.yml`, `claude-implement.yml`, `claude-review.yml`. Old repos +carry duplicates like `claude-review-max.yml` / `claude-implement-max.yml`. Fix: +delete the `-max` duplicates, keep the three canonical workflows. Severity: +**should** (blocker if a duplicate fires redundant/conflicting automation). + +**G. Missing `codeql.yml`.** The template ships a CodeQL workflow gated on +`use_node or use_python` (`template/.github/workflows/[% if use_node or use_python %]codeql.yml[% endif %].jinja`). +Repos with Node/Python code but no `codeql.yml` are missing static analysis. Fix: +add `codeql.yml` from the template (a re-template with the right answers includes +it). Severity: **should**. + +**H. lint-hygiene script portability to macOS bash 3.2.** `scripts/lint-hygiene.sh` +must be portable: **no `mapfile`, no `grep -P`** (both Linux/bash-4-only), and it +must self-skip in a path-independent way (skip any copy of the script so its own +pattern strings don't match) and skip symlinks (the AGENTS.md aliases). Old infra +copies use `mapfile` + `grep -P` and a brittle self-skip. Fix: replace with the +template version (`$TEMPLATE/scripts/lint-hygiene.sh`). Severity: **blocker** if +`task lint:hygiene` errors on macOS; otherwise **should**. + +**Also seeded from sourceRepoFollowUps (verify per repo):** legacy-bloated +`Brewfile` (deprecated formulae, missing gitleaks/yamllint/actionlint); CI that +reinstalls lint tools inline every run instead of using the prebuilt devcontainer +image / a composite action; auto-release-on-merge `release.yml` (standard is +release-please — an intentional rolling release PR, `use_release_please: yes` by +default; `task release:*` stays a manual override); stale `CHECKLIST.md` +(mentions pre-commit/cookiecutter); naming inconsistency between +workspace/bunch/slug files and the repo slug. Map each to its catalog area and +assign severity by the §2 rubric. + +--- + +## 4. Fix flow + +Apply fixes on a branch, prefer re-templating for files copier owns, then verify. + +1. **Branch.** Never commit to `main` (the template enforces a + `guard:no-commit-to-main` lefthook hook + a branch ruleset; respect it in the + target too). Create a feature branch, e.g.: + + ```bash + ( cd "$TARGET" && git switch -c chore/standardize-with-harmon-init ) + ``` + +2. **Apply fixes.** Two tracks — do the re-template first so hand-edits layer on + top of refreshed templated files: + + - **Templated bits — re-run copier.** For files the template owns, reconcile + rather than hand-porting: + - Generated from the template (has `.copier-answers.yml`): + ```bash + ( cd "$TARGET" && copier update --trust ) + ``` + - Never templated / adopting fresh: + ```bash + ( cd "$TARGET" && copier copy --trust ~/git/harmon-init . --vcs-ref=HEAD ) + ``` + `--vcs-ref=HEAD` is **load-bearing**: from a local path copier otherwise + renders the latest git tag and silently ignores committed-but-untagged work; + with it, copier includes dirty/untracked template changes via a throwaway + commit in a temp clone (your template working tree is untouched). Answer the + questions to match the repo, and keep all side-effectful answers + (`github_remote_create`, `github_release_init`, `bunch_add`, + `obsidian_project_add`, `run_task_install`) at their **no** defaults so the + adopt has no side effects. Review the resulting diff carefully and discard + overwrites of intentional local divergences. + + - **Non-templated bits — hand-edit.** Copier won't *delete* or *move* files, + so renames and relocations are manual: `git mv docs/specs specs`, + `git mv docs/runbook docs/runbooks`, delete `-max` + duplicate workflows, re-import the branch ruleset JSON, repoint the + AGENTS.md symlinks. Use the gap report's Reconciliation plan as the work + list. + +3. **Verify locally.** Run the same gates the standard requires, from the target: + + ```bash + ( cd "$TARGET" && task verify ) # check (lint) -> test:template + ( cd "$TARGET" && task security ) # gitleaks + dependency audit + ``` + + Fix anything red and re-run until clean. (`task verify` ≙ `task check` + + `task test:template`; `task ci` additionally chains `test` and `security`.) + +4. **Run the applied-state verifier.** Confirm the audited drift classes are + actually resolved by running the skill's checker: + + ```bash + ~/git/harmon-devkit/ai/skills/repo/standardize-repo/assets/verify-applied.sh "$TARGET" + ``` + + (See [`../assets/verify-applied.sh`](../assets/verify-applied.sh) — it should + re-check the §3 drift classes against the target and exit non-zero on any + remaining **blocker**. If the script isn't present yet in this skill, fall back + to manually re-walking the gap report's Reconciliation plan.) + +5. **Hand back.** Leave the changes committed on the feature branch with a + Conventional-Commits message (e.g. `chore: standardize against harmon-init`) + and open a PR for human + code-owner review — releases and merges stay + intentional; do not merge or tag. diff --git a/ai/skills/repo/standardize-repo/references/mode-new-repo.md b/ai/skills/repo/standardize-repo/references/mode-new-repo.md new file mode 100644 index 0000000..5fa1615 --- /dev/null +++ b/ai/skills/repo/standardize-repo/references/mode-new-repo.md @@ -0,0 +1,164 @@ +# Mode: New Repo — Scaffold from harmon-init + +Procedure for generating a brand-new repo from the +[harmon-init](https://github.com/evanharmon1/harmon-init) Copier template. Use +this when the destination directory does not yet exist (or is empty). To +standardize an *existing* repo instead, use the apply/update mode, not this one. + +The source of truth is `harmon-init/copier.yml`. Do not invent questions, task +names, or defaults — they are derived from that file below. + +## 1. Preconditions + +Verify before running anything: + +- [ ] **Tools installed:** `copier` (>= 9.4.0, per `_min_copier_version`), + `git`, and — if you will create the remote or release — `gh` (GitHub CLI, + authenticated: `gh auth status`). +- [ ] **Template available locally.** Default location is `~/git/harmon-init`. + If absent, clone it: `git clone https://github.com/evanharmon1/harmon-init ~/git/harmon-init`. +- [ ] **Destination does not already exist / is empty.** Copier writes into + ``; pick a path that is free. +- [ ] **Hidden author/org defaults are correct for you.** Identity, org info, + and machine-specific paths live in `copier.yml` under `when: false` + (e.g. `author_full_name`, `author_email`, `author_git_provider_username`, + `organization`, `projects_directory`, `bunches_directory`, + `obsidian_directory`). These are NOT asked interactively — they are baked + in for the template owner. If you are not the template owner, fork + harmon-init and edit those once before first use. + +## 2. Generate — interactive form + +The `--trust` flag is required: it allows copier to run the `_tasks` (git init, +commit, etc.) defined in `copier.yml`. + +```bash +copier copy ~/git/harmon-init --trust --vcs-ref=HEAD +``` + +(`harmon-init` here is the template source — usually a local checkout like +`~/git/harmon-init`. **From a local path, always pass `--vcs-ref=HEAD`** — without +it copier renders the **latest git tag** and silently ignores any +committed-but-untagged *and* uncommitted template work, the trap detailed in +`copier-gotchas.md`. Omit `--vcs-ref=HEAD` only when you deliberately want the last +tagged release, or when sourcing the template by its GitHub URL/a pinned tag.) + +Copier prompts for each asked question. Answer them; everything else falls back +to the hidden defaults. + +## 3. Generate — non-interactive form + +Supply answers with `--data key=value` (repeat per key). Side-effectful +questions all default to `no`, so omitting them is safe in CI. Add `--defaults` +to accept the default for any key you do not pass. + +```bash +copier copy harmon-init --trust --defaults \ + --data project_name="My Project" \ + --data project_slug="my-project" \ + --data project_description="One-line description of the project" \ + --data github_org="evanharmon1" \ + --data project_type="general" \ + --data include_terraform=false \ + --data include_ansible=false \ + --data ci_runner="ubuntu-latest" \ + --data license="mit" \ + --data use_release_please=true \ + --data devcontainer=true \ + --data git_init=true \ + --data github_remote_create=false \ + --data github_release_init=false \ + --data bunch_add=false \ + --data obsidian_project_add=false \ + --data run_task_install=false +``` + +### Answerable questions (from `copier.yml`) + +| Key | Type | Default | Choices / notes | +|---|---|---|---| +| `project_name` | str | — (required) | Formal name, e.g. "My Project". | +| `project_slug` | str | slugified `project_name` | lowercase, spaces → `-`. | +| `project_description` | str | `TODO: project_description` | Short description; replace the TODO. | +| `github_org` | str | `evanharmon1` (`author_git_provider_username`) | Org/user that owns the repo; drives repo URL, GHCR images, workflows. | +| `project_type` | str | `general` | `general` \| `web-astro` \| `web-app` \| `iac` \| `docs`. Drives Taskfile, CI jobs, devcontainer tooling. | +| `include_terraform` | bool | `true` iff `project_type == 'iac'` | Adds `terraform/` skeleton + terraform linting. | +| `include_ansible` | bool | `true` iff `project_type == 'iac'` | Adds `ansible/` skeleton + ansible linting. | +| `ci_runner` | str | `ubuntu-latest` | `ubuntu-latest` \| `self-hosted`. | +| `license` | str | `mit` | `mit` \| `private`. | +| `use_release_please` | bool | `true` | release-please rolling release PR + auto CHANGELOG. | +| `devcontainer` | bool | `true` | Dual-profile `.devcontainer` (AI bot + human dev). | +| `git_init` | bool | `true` | Initialize the git repo (see `_tasks`). | +| `github_remote_create` | bool | `false` | `gh repo create` (private, pushes initial state). | +| `github_release_init` | bool | `false` | Runs `task release:init` (initial release). | +| `bunch_add` | bool | `false` | Add Bunch file (macOS-only; moves to iCloud). | +| `obsidian_project_add` | bool | `false` | Add Obsidian project note to the vault (macOS-only). | +| `run_task_install` | bool | `false` | Run `task install` after generation (brew bundle + git hooks). | + +Notes: +- Several defaults are *computed* from earlier answers. Setting `project_type=iac` + flips `include_terraform`/`include_ansible` to `true` unless you override them. +- Hidden, derived flags you do **not** answer but that follow from your choices: + `use_node` (true for `web-astro`/`web-app`), `use_python` (true for `iac` or + `include_ansible`), `repo_url`, `devcontainer_image`, `ci_runner_labels`. + +## 4. Post-generation `_tasks` (run automatically, in order) + +Because `--trust` was passed, copier runs the `_tasks` from `copier.yml` +**after** rendering, in this exact order. Each is gated on the answer in +brackets; all side-effectful ones default to `no` so `copier copy --defaults` +is CI-safe (only `git_init` runs by default, and it only touches the new +project directory): + +1. `git init -b main` — when `git_init`. +2. `git add -A && git commit -m "chore: initial scaffold from harmon-init"` — + when `git_init`. The initial commit exists so steps 3 and 5 have a `HEAD`. + It runs *before* `task install`, so lefthook hooks are not yet installed and + nothing intercepts this commit. +3. `gh repo create / --private --source=. --push` — + when `github_remote_create`. +4. `task install` — when `run_task_install` (brew bundle + `lefthook install`, + plus `uv sync` / `pnpm install` if applicable). +5. `task release:init` — when `github_release_init` (tags `v0.1.0`, pushes it, + `gh release create`). Requires the remote to exist (step 3). +6. `task util:bunch-add` — when `bunch_add` (macOS-only). +7. `task util:obsidian-add` — when `obsidian_project_add` (macOS-only). + +If you left the side-effectful answers at their `no` defaults (the CI-safe, +recommended path for unattended generation), only steps 1–2 run and you finish +setup manually in the next section. + +## 5. After generation — local setup & self-check + +```bash +cd + +# If you did NOT set run_task_install=true, do it now: +task install # Brewfile deps (+ uv sync / pnpm install as applicable) + lefthook hooks + +task verify # lint + (template's) checks — the local merge gate + +# Skill self-check that the conventions actually landed: +bash /assets/verify-applied.sh +``` + +`` is the root of this skill +(`.../ai/skills/repo/standardize-repo`). `assets/verify-applied.sh` asserts the +expected artifacts are present (e.g. `Taskfile.yml`, `lefthook.yml`, the +`AGENTS.md` symlinks, `.github/workflows/`). Investigate any failure before +proceeding. + +## 6. Hand off — GitHub setup + +Finish remote/GitHub configuration via the generated checklist and this skill's +companion reference: + +- In the new repo: work through `docs/CHECKLIST.md` (rendered from + `template/docs/CHECKLIST.md.jinja`). It covers, in order: local setup → GitHub + repo settings (branch ruleset import via `gh api ... rulesets`, Dependabot + alerts + private vulnerability reporting, Renovate app, CodeRabbit app, Actions + secrets/variables, the CI GitHub App, GHCR publishing) → framework scaffolding + for the chosen `project_type` → secrets/env → docs/meta (fill `TODO:` markers, + confirm badges, optional `task release:init`). +- Then follow **`references/post-generation-checklist.md`** in this skill for the + agent-driven walkthrough of that GitHub setup. diff --git a/ai/skills/repo/standardize-repo/references/post-generation-checklist.md b/ai/skills/repo/standardize-repo/references/post-generation-checklist.md new file mode 100644 index 0000000..259ffef --- /dev/null +++ b/ai/skills/repo/standardize-repo/references/post-generation-checklist.md @@ -0,0 +1,284 @@ +# Post-Generation Checklist + +Steps to run **after** `copier copy` finishes and the generated files are +committed to the repo. Generalized from harmon-init's own +`template/docs/CHECKLIST.md.jinja`; see also `docs/architecture/security.md` for +the GitHub App rationale. + +Every step is tagged **[scriptable via gh]** (an agent can run it +non-interactively) or **[human-only]** (requires a browser/UI, a one-time secret +the agent must not fabricate, or a deliberate manual action per the security +model). For scriptable steps the exact `gh` command is given. + +Throughout, substitute the copier answers: + +- `` — the `github_org` answer (defaults to the author's username; an + **org** repo is one where `github_org != author_git_provider_username`). +- `` — the `project_slug` answer. +- `` — one of `general`, `web-astro`, `web-app`, `iac`, `docs`. + +Run from the generated repo's root, on the default branch, after the first +push so the remote exists. + +--- + +## 1. Local setup + +- [ ] **[scriptable via gh]** Install deps + git hooks. Installs Brewfile deps + and, depending on copier answers, `uv sync` (Python) / `pnpm install` + (Node) plus lefthook hooks. + + ```bash + task install + ``` + +- [ ] **[scriptable via gh]** Confirm the full local gate passes before relying + on CI. + + ```bash + task verify + ``` + +--- + +## 2. GitHub repo settings + +- [ ] **[scriptable via gh]** Import the branch ruleset that protects `main` + (required reviews + the `verify`/`security` status checks). The JSON is + generated into the repo's `.github/`: + + ```bash + gh api "repos///rulesets" --method POST \ + --input ".github/Branch Protection Ruleset - Protect Main.json" + ``` + +- [ ] **[scriptable via gh]** Enable **Dependabot alerts**. Do NOT add a + `dependabot.yml` — Renovate owns version updates; Dependabot is alerts-only. + + ```bash + gh api "repos///vulnerability-alerts" --method PUT + ``` + +- [ ] **[scriptable via gh]** Enable **private vulnerability reporting**. + + ```bash + gh api "repos///private-vulnerability-reporting" --method PUT + ``` + +- [ ] **[human-only]** Install the **Renovate** GitHub App on the repo — + (the generated `renovate.json` is + pre-configured). App installation goes through GitHub's UI consent flow. + +- [ ] **[human-only]** Install the **CodeRabbit** GitHub App on the repo — + (the generated `.coderabbit.yaml` is + pre-configured). UI consent flow. + +- [ ] **[human-only]** Set Actions **secret** `CLAUDE_CODE_OAUTH_TOKEN` + (consumed by `claude-plan.yml`, `claude-implement.yml`, + `claude-review.yml`). This is a real credential the agent must not invent; + a human pastes the token. Once you have it: + + ```bash + # human supplies the token value; do not fabricate it + gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo "/" + ``` + +- [ ] **[human-only]** Set Actions **secret** `SNYK_TOKEN` (consumed by + `task security:sast` / `task security:sca`). Real credential — human + supplies it: + + ```bash + gh secret set SNYK_TOKEN --repo "/" + ``` + +- [ ] **[human-only]** Create or reuse the CI **GitHub App** `-ci`, then + set `CI_APP_ID` (Actions **variable**) + `CI_APP_PRIVATE_KEY` (Actions + **secret**). This App authenticates `release.yml` (release-please) and the + `claude-*` workflows; minting an App-authored commit is what lets a release + PR's required checks actually run (the built-in `GITHUB_TOKEN` would not + retrigger CI). + + **One App per org** (or per personal account) — `-ci`, e.g. + `evanharmon1-ci` — so a leaked key is contained to one org. The App must be + created in the GitHub UI: the app-manifest "one-click" flow can't be completed + by a static page (the one-time `?code=` expires), so this is human-only. The + exact permission set is checked into the generated repo as + `.github/github-app-manifest.json`; mirror it in the form. + + - New GitHub App: org → `https://github.com/organizations//settings/apps/new`; + personal → `https://github.com/settings/apps/new`. + - Name `-ci`; uncheck the **Active** webhook; **"Only on this account"**; + grant exactly these permissions and nothing more: + + | Permission | Level | Why | + |---|---|---| + | Contents | Read and write | commits, branches, tags, releases | + | Pull requests | Read and write | open/update the release PR and claude PRs | + | Issues | Read and write | claude comments/labels/updates issues | + | Workflows | Read and write | claude may edit files under `.github/workflows/` | + | Metadata | Read-only | required baseline | + + - Generate a private key (`.pem`) and note the **App ID**. + - **Install App** → on this org → **Only select repositories** (not "All"). + - Set the variable + secret. **Do this by hand and do not script org-scoped + secret-setting** — the bulk `--repos` form *replaces* the secret's value and + its repo allow-list, silently evicting other repos. For a **personal-account + repo** the per-repo form is safe: + + ```bash + # personal-account repo only; org-level should be set in the UI / non-destructively + gh variable set CI_APP_ID --repo "/" --body "" + gh secret set CI_APP_PRIVATE_KEY --repo "/" < path/to/app.pem + ``` + + See `docs/architecture/security.md` for blast-radius and rotation notes. + +- [ ] **[scriptable via gh]** Enable **CodeQL** by setting the Actions variable + `FULL_SECURITY_SCAN=true` (the generated `codeql.yml` is gated + `if: vars.FULL_SECURITY_SCAN == 'true'`; only present when the project uses + Node and/or Python): + + ```bash + gh variable set FULL_SECURITY_SCAN --repo "/" --body "true" + ``` + +- [ ] **[human-only]** (devcontainer projects) Ensure the org/user **allows + GHCR package publishing** so the first `devcontainer-build.yml` prebuild on + merge to main can populate `ghcr.io//-devcontainer`. The + workflow already requests `packages: write` and logs in with + `GITHUB_TOKEN`; org package-creation policy is a UI setting. + +### Org repos only (`github_org != author_git_provider_username`) + +- [ ] **[scriptable via gh]** Create a **Project V2** so that, after linking, it + is **project number 1** for the org, with a `Status` single-select field + (`project-automation.yml` and the `claude-*` workflows drive it). Use the + GraphQL API via `gh`: + + ```bash + # 1. org node id + ORG_ID=$(gh api graphql -f query='query($l:String!){organization(login:$l){id}}' \ + -f l="" --jq '.data.organization.id') + + # 2. create the project (note its number in the response) + gh api graphql -f query='mutation($o:ID!,$t:String!){createProjectV2(input:{ownerId:$o,title:$t}){projectV2{id number}}}' \ + -f o="$ORG_ID" -f t="" + + # 3. add the Status single-select field (capture PROJECT_ID from step 2) + gh api graphql -f query='mutation($p:ID!){createProjectV2Field(input:{projectId:$p,dataType:SINGLE_SELECT,name:"Status",singleSelectOptions:[{name:"Shaping",color:GRAY,description:""},{name:"In Progress",color:BLUE,description:""},{name:"Validating",color:YELLOW,description:""},{name:"In Review",color:PURPLE,description:""},{name:"Done",color:GREEN,description:""}]}){projectV2Field{... on ProjectV2SingleSelectField{id}}}}' \ + -f p="" + ``` + + > Note: "number 1" assumes this is the org's first Project V2. If other + > projects exist, the new project's number will differ — record the actual + > number and reconcile it with whatever the automation workflow references. + > TODO: confirm whether `project-automation.yml` hardcodes project number 1 + > or reads it from a variable. + +- [ ] **[scriptable via gh]** Add the bot machine account + (`-bot`) as a **Write** collaborator (it does + the in-container git pushes; it cannot merge `main`): + + ```bash + gh api "repos///collaborators/-bot" \ + --method PUT -f permission=push + ``` + +--- + +## 3. Framework scaffolding (conventions-only template) + +The template ships conventions, not an application. Scaffold the framework that +matches ``: + +- [ ] **[scriptable via gh]** (`web-astro`) Scaffold Astro and add the standard + stack: + + ```bash + pnpm create astro@latest . --template minimal + pnpm add -D @tailwindcss/vite vitest + pnpm add zod lucide + ``` + + Then move lint tooling (prettier, eslint, markdownlint-cli2, @commitlint/cli) + into `devDependencies` and switch the generated `Taskfile.yml`'s `npx --yes` + calls to `pnpm exec`. Review `lighthouserc.json` URLs once routes exist. + +- [ ] **[scriptable via gh]** (`web-app`) Scaffold a TanStack Start app (or + vite + react) and add the standard stack: + + ```bash + pnpm create @tanstack/start@latest # or: pnpm create vite@latest . -- --template react-ts + pnpm add -D vitest + pnpm add zod lucide + # shadcn/ui + Tailwind v4 per their installers, e.g.: + pnpm dlx shadcn@latest init + ``` + + Move lint tooling into `devDependencies` and switch Taskfile `npx --yes` calls + to `pnpm exec`. + +- [ ] **[scriptable via gh]** (`iac`) Lay out the IaC tree. Lint tasks for + Ansible activate automatically once `ansible/site.yml` exists: + + ```bash + mkdir -p terraform ansible/inventory ansible/roles + : > terraform/main.tf + : > terraform/variables.tf + : > terraform/outputs.tf + : > ansible/site.yml + : > ansible/ansible.cfg + ``` + +- [ ] **[human-only]** (`docs`) Decide the docs toolchain (plain markdown / + Obsidian vault / static-site generator) — a judgment call, not scripted. + +- [ ] **[scriptable via gh]** (`general` / anything else) Add the project's + primary toolchain and extend the Taskfile `build`/`test` targets + accordingly. + +--- + +## 4. Secrets & environment + +- [ ] **[human-only]** For local `.env` needs, use **1Password Environments** + (mounts a virtual `.env` over a UNIX pipe — values never hit disk or git) + or `op run` / `op inject`. Commit only `.env.example`-style files. + +- [ ] **[human-only]** (devcontainer projects) Devcontainer secrets: create a + **1Password environment** with destination "Local .env file" mounted at + `.devcontainer/devcontainer.env` (and `.devcontainer/dev/devcontainer.env`), + holding `GH_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN`, `AGENT_DECK_TELEGRAM_KEY` + (+ `TS_AUTHKEY` for the dev profile; `ANTHROPIC_API_KEY` is forbidden). + `init-env.sh` only enforces the per-profile allow-list and seeds from the + **host env** (the Coder/Codespaces path) — it does **not** call `op`. Full + walkthrough in the generated repo's `docs/guides/devcontainers.md`. + +- [ ] **[human-only]** (Coder) To run the devcontainer in Coder, create a + workspace from the **org-level Coder devcontainer template** (not part of + the repo — canonical example: `terraform/coder/devcontainer/` in + harmonops/harmon-infra) with its `repo` parameter set to this repo and the + secret parameters above (Coder passes them as host env → `init-env.sh`). + The build pulls `` from GHCR as a cache (private + package ⇒ give the builder a read token; a miss only slows the first build). + +--- + +## 5. Docs & meta + +- [ ] **[human-only]** Fill in the `TODO:` markers in `README.md` and `docs/` + (architecture diagram first) — authoring, not scripting. + +- [ ] **[scriptable via gh]** Confirm README badges render once CI has run + (Actions URLs become valid after the first workflow run): + + ```bash + gh run list --repo "/" --limit 5 + ``` + +- [ ] **[human-only]** Cut the initial release when ready — releases stay + **intentional**, never automated on merge: + + ```bash + task release:init # v0.1.0 + ``` diff --git a/ai/skills/repo/standardize-repo/references/standards-catalog.md b/ai/skills/repo/standardize-repo/references/standards-catalog.md new file mode 100644 index 0000000..2fef211 --- /dev/null +++ b/ai/skills/repo/standardize-repo/references/standards-catalog.md @@ -0,0 +1,557 @@ +# Standards Catalog + +The authoritative list of attributes a repo should have to conform to the +[harmon-init](https://github.com/evanharmon1/harmon-init) Copier template +conventions. An auditing or scaffolding agent consults this to answer: **"what +should this repo have, and where does it come from?"** + +Ground-truth sources (read these, don't trust memory): `harmon-init/copier.yml`, +`harmon-init/AGENTS.md`, the `harmon-init/template/` tree. Live reference repos +that have been generated from the template: `harmonops/harmon-infra` (an `iac` +project) and `sommerlawn/sommerlawn-web` (a `web-astro` project). Note: the live +repos are pinned to **older template commits** (e.g. harmon-infra `_commit: +v2.3.1`) and predate several current conventions (release-please, the +AGENTS.md-canonical symlink flip, the dual-profile devcontainer, the +`docs/product|architecture|decisions|guides|runbooks` layout). Treat the +**template** as canonical; treat divergences in the live repos as either legit +project-type specifics (Part 3) or as drift the repo itself would need to update. + +Every item is tagged: + +- **[copier]** — the template generates this automatically; a freshly generated + repo already has it. An audit flags it only if missing/modified. +- **[manual]** — a follow-up the operator/agent must do by hand (it lives in + `docs/CHECKLIST.md`, depends on a side-effectful copier answer that defaults to + `no`, or requires a GitHub/external action copier can't perform). + +--- + +## Part 1 — Universal conventions (every repo) + +These apply regardless of `project_type`. + +### 1.1 Docs-folder layout + +The `docs/` tree is a **routing hub** ("routes; does not hold facts"). Folder +landing pages are always `README.md`. **[copier]** generates the whole skeleton. + +| Path | Purpose | Source | +|---|---|---| +| `docs/README.md` | Hub; the four-buckets table | [copier] | +| `docs/conventions.md` | Flat-lookup of enforced rules (grep, don't read) | [copier] | +| `docs/glossary.md` | Term → definition flat lookup | [copier] | +| `docs/CHECKLIST.md` | Run-once post-generation setup list | [copier] | +| `docs/product/` | Why it exists / who for — `vision.md`, `roadmap.md`, `domain.md`, `README.md` | [copier] | +| `docs/architecture/` | How it's built — `README.md`, `ci-cd.md`, `security.md`, `branch-protection.md`, `tests.md` (+ `design-language.md` for web types) | [copier] | +| `docs/decisions/` | ADRs, numbered `0001-`, zero-padded; `0001-record-architecture-decisions.md` ships as the template ADR; `README.md` index | [copier] | +| `docs/guides/` | Calm how-tos read in advance — `onboarding.md`, `deploying.md`, `troubleshooting.md`, `README.md` | [copier] | +| `docs/runbooks/` | Crisis procedures read under pressure — `README.md` | [copier] | + +Repo-root siblings of `docs/` (deliberately NOT under `docs/`): + +| Path | Purpose | Source | +|---|---|---| +| `specs/` | Source of truth for **WHAT to build**; `_template.md` + `README.md`; one spec per feature, Given/When/Then acceptance criteria | [copier] | +| `tests/` | Test files live here; ships with a `.gitkeep` | [copier] | + +- **ADR rules:** one ADR per decision; immutable once Accepted; to change a + decision add a new ADR that supersedes and update the old one's Status + (Proposed / Accepted / Deprecated / Superseded). Sections: Status, Context, + Decision, Consequences. +- **`.gitkeep`** keeps otherwise-empty dirs in git (`tests/.gitkeep`, + `.claude/skills/.gitkeep`, and ansible `roles|playbooks|inventory/.gitkeep` + for iac). Across the live repos, `docs/` subdirs are also kept with `.gitkeep` + (harmon-infra: `architecture/decisions/specs`; sommerlawn-web: + `decisions/runbooks/specs`). +- **Filling content is [manual]:** most generated docs carry literal `TODO:` + markers (e.g. `security.md`, `design-language.md`, `DESIGN.md`); the operator + fills them in. The CHECKLIST item "Fill in the `TODO:` markers" tracks this. + +### 1.2 Taskfile (`Taskfile.yml`, go-task v3) + +**[copier]** generates `Taskfile.yml`. (`Taskfile.yaml` is equally valid — go-task +accepts both; use whichever extension the tool conventionally uses, don't +normalize.) The Taskfile is the **single +source of truth for commands** — lefthook hooks and CI workflows delegate to +`task` targets so local/CI/hook runs are byte-identical. Never reimplement command +logic in a workflow or a hook. + +Naming & structure conventions: + +- **`group:action` (kebab + colon namespacing).** Group/domain first, action + leaf last: `lint:shell`, `lint:terraform:validate`, `test:e2e`, + `security:secrets`, `install:hooks`, `status:git`, `deploy:ansible:base`. + **Never action-first** (`shell:lint`, `yaml:lint`). +- **Pipeline order:** `check → build → validate → test → security`, with + `verify` (local gate) and `ci` (full) as aggregates. `verify` = + `check [→ build] → validate`. +- **Parallel deps:** umbrella tasks fan out via `deps:` (which run in parallel), + e.g. `lint` deps on `lint:yaml`, `lint:shell`, `lint:markdown`, + `lint:actions`, `lint:hygiene`; `security` deps on `security:secrets` + + `security:audit`; `check` deps on `lint` (+ `typecheck`). +- **`{{.CLI_ARGS | default "."}}` passthrough:** lint tasks accept a file list + (`task lint:yaml -- file.yml`) and default to the whole tree; lefthook passes + `{staged_files}` through this so hooks lint only staged files. +- **Group output:** `output.group` wraps each task's output in + `::group::{{.TASK}}` / `::endgroup::` for collapsible CI logs. +- **`default`** task is an interactive `tv`/`fzf` task menu. + +Universal task targets every repo has (from the template): + +`default`, `menu`, `menu-tv`, `ci`, `verify`, `check`, `lint`, `lint:yaml`, +`lint:shell`, `lint:markdown`, `lint:actions`, `lint:hygiene`, +`lint:commit-msg`, `validate`, `guard:no-commit-to-main`, `format`, `fix`, +`test`, `security`, `security:secrets`, `security:audit`, `security:sast` +(Snyk), `security:sca` (Snyk), `bootstrap`, `install`, `install:hooks`, +`release:init`, `release:patch`, `release:minor`, `release:major`, `clean`, +`status` (+ `status:git|gh|code|env`), `util:bunch-add`, `util:obsidian-add`. + +Notable command bodies (for an auditor checking they match): + +- `lint:shell` → `shellcheck --severity=error` + `shfmt -d` +- `lint:markdown` → `npx --yes markdownlint-cli2 --fix '**/*.md' '#.claude/**' …` +- `lint:hygiene` → `./scripts/lint-hygiene.sh` +- `security:secrets` → `gitleaks detect --no-banner --redact --source .` +- `install` → `brew bundle --file=Brewfile` (+ `uv sync` / `pnpm install`) → + `install:hooks` +- `install:hooks` → `lefthook install` + +### 1.3 File/dir naming, branch & commit conventions + +- **Doc filenames are kebab-case** (`branch-protection.md`, `ci-cd.md`). The + conventional uppercase root files keep their names: `README.md`, `AGENTS.md`, + `DESIGN.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, + `LICENSE`, `CHECKLIST.md`. **[copier]** +- **YAML extensions follow each tool's own convention** — no repo-wide + `.yml`-vs-`.yaml` normalization (go-task → `Taskfile.yml`, CodeRabbit → + `.coderabbit.yaml`). Don't rename a tool's file to homogenize extensions. +- **Feature branches only.** Direct commits to `main` are blocked by the + `guard:no-commit-to-main` pre-commit hook AND the branch ruleset. **[copier]** + for the hook/ruleset file; **[manual]** to import the ruleset to GitHub. +- **Claude bot branches** are prefixed `claude/` (set in claude workflows and + parsed by project-automation as `claude/issue-N`). +- **Conventional Commits**, enforced by commitlint (`commit-msg` hook). The + **type enum** (read from `commitlint.config.mjs`, identical in template and + both live repos): + + ``` + build, change, chore, ci, docs, feat, fix, perf, refactor, remove, revert, style, test + ``` + + Format `type(scope): subject`, imperative mood. Subject/body lines ≤ 100 chars + (config-conventional). Breaking changes: `feat!:` or `BREAKING CHANGE:` footer. + **[copier]** ships `commitlint.config.mjs` extending + `@commitlint/config-conventional`. +- **`TODO:` prefix** (literal, colon) marks unfinished work in code and docs so + it stays greppable (`rg 'TODO:'`). +- **Never bypass hooks** — `--no-verify` is forbidden (and actively blocked by a + Claude hook in the devcontainer). + +### 1.4 Lint / format configs + +All **[copier]** (root-level config files): + +| File | Tool | Key settings | +|---|---|---| +| `.editorconfig` | EditorConfig | `root=true`; default 2-space, `lf`, utf-8, final newline, trim trailing ws; **4-space for `*.{py,tf,tfvars,sh}`**; 2-space TOML; Markdown/MDX keep trailing ws (intentional `
`); Makefiles tab | +| `.yamllint` | yamllint | `extends: default`; ignores `node_modules/ dist/ .astro/ .task/ .worktrees/ .venv/ .terraform/ pnpm-lock.yaml …`; `line-length max 200` (warn); comments/document-start/truthy disabled | +| `.shellcheckrc` | shellcheck | `disable=SC3037`, `disable=SC2148` | +| `.markdownlint.json` | markdownlint | `default: true`; MD003 atx; MD013 (line-length), MD034, MD036, MD041, MD060 off; blanks-around-headings/lists off | +| `.markdownlint-cli2.jsonc` | markdownlint-cli2 | only when `use_release_please`; `ignores: ["CHANGELOG.md"]` (release-please writes double blanks → MD012) | + +Shell scripts must pass `shellcheck --severity=error` + `shfmt -d` and stay +portable to **macOS bash 3.2** (no `mapfile`, no `grep -P`) and Linux. + +Conditional formatters/linters (see Part 2 for which project types): + +- **prettier** — `prettier.config.cjs` (web only): `prettier-config-standard` + base, `singleQuote`, no semi, `trailingComma:none`, `printWidth:100`, + `prettier-plugin-astro` + `prettier-plugin-tailwindcss`. +- **eslint** — web only (config file is added during framework scaffolding, + [manual]; template provides the `lint:eslint` task that calls `npx eslint`). +- **black** — Python only (`lint:python` → `uv run black --check`). +- **terraform fmt / validate** — iac/terraform only. +- **ansible-lint** — `.ansible-lint` (ansible only). + +### 1.5 Git hooks (lefthook) + +**[copier]** ships `lefthook.yml`; installed via `task install:hooks` (`lefthook +install`). `assert_lefthook_installed: true`. Every hook **delegates to a Taskfile +target**. + +| Stage | Commands (universal) | +|---|---| +| `pre-commit` (parallel) | `no-commit-to-main` (`task guard:no-commit-to-main`), `yaml`, `shell`, `markdown`, `actions`, `hygiene` — each globbed, passing `{staged_files}` | +| `commit-msg` | `conventional` → `task lint:commit-msg -- {1}` | +| `pre-push` (parallel) | `secrets` → `task security:secrets` | + +Project-type stages (added conditionally): pre-commit `prettier`/`eslint`/ +`typecheck` (node), `python` (python), `terraform` (terraform); pre-push +`typecheck` (node), `terraform-validate` (terraform), `ansible-syntax` (ansible). + +### 1.6 Devcontainer (dual-profile) — when `devcontainer: yes` (default) + +**[copier]** generates `.devcontainer/` with **two profiles**: + +- **BOT profile** (`.devcontainer/devcontainer.json`) — for AI agents (Claude + Code, Codex, Gemini). **No Tailscale.** `containerName: + devcontainer--bot`. `CLAUDE_CODE_EFFORT_LEVEL: max`. +- **DEV profile** (`.devcontainer/dev/devcontainer.json`) — human dev. Adds the + Tailscale feature + `--device=/dev/net/tun` + `TS_AUTHKEY`. + +Shared structure: + +- **`Dockerfile`** — single Dockerfile, base + `mcr.microsoft.com/devcontainers/base:ubuntu-24.04`. Tool version pins are + **`ARG _VERSION=…` annotated with `# renovate: datasource=… depName=…`** + comments (Renovate's regex manager auto-PRs bumps). `NODE_MAJOR` is + intentionally unmanaged. Layers ordered cheap→volatile (volatile npm globals + like `@anthropic-ai/claude-code` LAST so frequent bumps don't bust the + Chromium/Playwright layers). +- **`devcontainer.json` `features`:** python 3.14, docker-in-docker, github-cli, + go-task, 1password; terraform feature when `include_terraform`; tailscale only + in dev. +- **`config/`** — baked dotfiles (zshrc, starship, tmux, zellij, micro, gitconfig, + television, agent-deck) + **`config/claude-settings.json`** (installed as Claude + Code **managed settings** at `/etc/claude-code/managed-settings.json`, highest + precedence) + **`config/claude-hooks/`** (5 hooks: `protect-files.sh`, + `block-no-verify.sh`, `enforce-conventional-commits.sh`, `post-edit-format.sh`, + `session-start-context.sh`, installed to `/etc/claude-code/hooks/`). +- **Secret standard — 1Password Environments. [manual]** The values in + `.devcontainer/devcontainer.env` (+ `dev/devcontainer.env`) come from a + **1Password environment** with destination "Local .env file" mounted at those + paths — a virtual `.env` over a UNIX pipe, never written to disk or git. Vars: + `GH_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN`, `AGENT_DECK_TELEGRAM_KEY` (+ `TS_AUTHKEY` + dev-only). +- **`scripts/init-env.sh`** — runs as `initializeCommand` on the HOST. It does + **not** call `op`; it enforces the per-profile allow-list (evicts forbidden + vars — bot strips `TS_AUTHKEY`; **strips `ANTHROPIC_API_KEY` unconditionally** + since it silently overrides `CLAUDE_CODE_OAUTH_TOKEN`) and seeds the env-file + from the **host environment** — the path used on **Coder/Codespaces**, where + secrets arrive as workspace/template parameters. Portable to BSD/macOS sed. +- **`post-create.sh` / `post-start.sh`** (+ `dev/` variants) and + `scripts/post-create-common.sh`; bot sets git identity to `-bot`. +- **`hooks/post-checkout`** — repo-managed git hook (auto-installs node_modules in + new worktrees). +- **GHCR prebuild:** images push to `ghcr.io//-devcontainer[-dev]` + (`devcontainer_image`) via the `devcontainer-build.yml` workflow as build + caches. **[manual]** GHCR publishing permission + first prebuild on merge. +- **Coder. [manual]** The devcontainers are Coder-ready (CODER passthrough; + `config/` baked to `/usr/local/share/devcontainer-config/` to survive Coder's + `/tmp` shadowing). The Coder workspace *template* is **org-level infra, not + per-repo** (canonical: harmon-infra `terraform/coder/devcontainer/`): point its + `repo` + secret parameters at the repo → host env → `init-env.sh`. The + generated repo's `docs/guides/devcontainers.md` has the full walkthrough. +- **`devcontainer.env`** is gitignored; only `devcontainer.env.example` is + committed. **[manual]** to populate real secrets. +- Smoke tests: `task test:devcontainer:root` / `test:devcontainer:dev`. + +### 1.7 CI/CD (GitHub Actions) + +**[copier]** generates all workflows. Cross-cutting rules: + +- **Delegate to `task` targets** (`task check`, `task security`, `task build`, + `task test`). +- **Pin third-party actions by full commit SHA** + trailing `# vX.Y.Z` comment; + annotate tool versions with `# renovate: datasource=…`. +- **Least-privilege `permissions:`** per job (top-level `contents: read`). +- **`merge_group`** trigger on `build.yml` (merge-queue support). +- **Aggregate gate:** a final `verify` job (`if: always()`, `needs: [lint, + security, …]`) reports one rollup status. Branch protection requires the + **`verify`** and **`security`** checks. +- Fork-PR guard: jobs gate on + `github.event.pull_request.head.repo.full_name == github.repository`. + +Workflow inventory: + +| Workflow | Triggers / role | Source | +|---|---|---| +| `build.yml` (`Build & Validate`) | push/PR/`merge_group`/dispatch; jobs `lint`, `security` (+ `build-test` node, `lighthouse` web-astro), aggregate `verify` | [copier] | +| `claude-plan.yml` | `@claude plan` / `claude-plan` label → posts a plan, no writes (`--disallowedTools Edit Write Bash`, `--model opus`) | [copier] | +| `claude-implement.yml` | `@claude implement` / label → opens a PR on a `claude/` branch (`--model sonnet`) | [copier] | +| `claude-review.yml` | `@claude review` / label → review comment, no writes (sticky comment) | [copier] | +| `release.yml` | release-please; only when `use_release_please` | [copier] | +| `codeql.yml` | only when `use_node or use_python`; **opt-in via `FULL_SECURITY_SCAN=true`** variable; aggregate `codeql-verify` | [copier] | +| `devcontainer-build.yml` | only when `devcontainer`; builds bot+dev images, pushes GHCR caches on merge to main | [copier] | +| `project-automation.yml` | only when `github_org != author_git_provider_username` (org repos); syncs org Project V2 Status field | [copier] | + +**GitHub App auth** (the claude-* workflows, `release.yml`, and +`project-automation.yml`): authenticate as a **`-ci` GitHub App** (one App +per org/account), not a PAT. Each job mints a short-lived (~1h) token via +`actions/create-github-app-token` reading **`CI_APP_ID`** (Actions **variable**) + +**`CI_APP_PRIVATE_KEY`** (Actions **secret**), with **least-privilege +`permission-*` inputs** (e.g. plan: `permission-contents: read` + +`permission-issues|pull-requests: write`; implement adds +`permission-workflows: write`; org repos add `permission-organization-projects: +write`, `permission-members: read`). Requesting a permission the installation +lacks fails token minting — that's why org-only perms are jinja-gated. +`.github/github-app-manifest.json` is the machine-readable permission reference. +**[manual]:** create the App, install it on the repo, set the variable + secret. + +Required secrets/variables (**[manual]**, in CHECKLIST): `CLAUDE_CODE_OAUTH_TOKEN` +(secret), `SNYK_TOKEN` (secret), `CI_APP_ID` (variable) + `CI_APP_PRIVATE_KEY` +(secret), `FULL_SECURITY_SCAN=true` (variable, to enable CodeQL). + +### 1.8 Security + +- **gitleaks** — `.gitleaks.toml` (`[extend] useDefault = true` + an + `[allowlist] paths` of build/cache dirs). Runs at pre-push (`task + security:secrets`) and in the `build.yml` `security` job (with the + `summarize-gitleaks.mjs` GH step summary). **[copier]** +- **Snyk** — `task security:sast` (`snyk code test`) + `security:sca` (`snyk + test`); needs `SNYK_TOKEN`. **[copier]** for tasks; **[manual]** for the token. +- **CodeQL** — `codeql.yml`, opt-in via `FULL_SECURITY_SCAN`. **[copier]** / + **[manual]** to enable. +- **Branch protection ruleset** — `.github/Branch Protection Ruleset - Protect + Main.json`: blocks deletion/non-ff/creation, requires linear history, PR with 1 + code-owner approval + thread resolution + last-push approval, required status + checks `verify` + `security`, merge methods squash/rebase; org repos add a + `merge_queue` rule. **[copier]** ships the file; **[manual]** import via + `gh api repos///rulesets --method POST --input "…"`. +- **`SECURITY.md`** lives in **`.github/`** (Private Vulnerability Reporting). + **[copier]** +- **Renovate, NOT Dependabot** for version updates. CHECKLIST explicitly says + enable Dependabot *alerts* + Private vulnerability reporting but **do NOT add + `dependabot.yml`** — Renovate owns updates. **[manual]** repo settings. +- **`CODEOWNERS`** = `* @`. **[copier]** +- **Secrets via 1Password** locally (`op run`/`op inject`); CI reads Actions + secrets. **`.env` is fully gitignored** (`.env`, `**/.env`, `.env.*`); commit + only `.env.example`-style files. **[copier]** gitignore; **[manual]** wiring. + +### 1.9 Dependency management (Renovate) + +**[copier]** ships `renovate.json` (extends `config:recommended`). **[manual]** +install the Renovate GitHub App on the repo. Conventions: + +- **`automerge: false`** globally; **`minimumReleaseAge: 3 days`** on all + packages (stability gate). +- **Custom (regex) managers** for pins invisible to native managers: + - Devcontainer **Dockerfile `ARG …_VERSION`** annotated with `# renovate:`. + - **Workflow tool pins** — both `version:` style and `FOO_VERSION=x.y.z` + shell-variable style, under `.github/workflows/`. + - Ansible: annotated container images + `*_version` vars (iac only). +- **Batching (groupName):** GitHub Actions, Docker images, Devcontainer, npm, + Terraform providers each into one PR. +- **`anthropics/claude-code-action` ejection:** removed from the Actions group + (`groupName: null`) and `minimumReleaseAge: 0 days` (ships near-daily; grouping + + the 3-day gate kept the whole batch perpetually pending). Rule ordered AFTER + the group rules so the override wins. +- npm `overrides` deptype disabled (avoids `EOVERRIDE`). +- `dependencyDashboard: true`; weekly schedule `before 9am on Monday`, + `timezone: America/Chicago`. + +### 1.10 AI steering + +- **`AGENTS.md` is the canonical source of truth.** `CLAUDE.md`, `GEMINI.md`, and + `.github/copilot-instructions.md` are **symlinks** to it — edit only + `AGENTS.md`. (`copier.yml` sets `_preserve_symlinks: true`.) Live repos at older + commits have the symlink flipped the other way (`AGENTS.md -> CLAUDE.md`) — see + Part 3. **[copier]** +- **`.claude/settings.json`** (repo-level): minimal allow-list — + `Bash(task:*)`, `Bash(git status:*)`, `Bash(git diff:*)`, `Bash(git log:*)`. + **[copier]** +- **`.claude/skills/`** with a `.gitkeep`. **[copier]** +- Devcontainer ships richer `config/claude-settings.json` as managed settings (see + 1.6). **[copier]** +- **`DESIGN.md`** — AI-facing statement of design intent (the *why*/prose rules); + web types get a "Visual & UX direction" section. **[copier]** ships it with + `TODO:` placeholders; **[manual]** to fill in. + +### 1.11 Package / tool management + +- **`Brewfile`** — pins the core toolchain (go-task, lefthook, git, gh, + shellcheck, shfmt, actionlint, yamllint, gitleaks, snyk, node, jq, fzf, fd, + ripgrep, bat; conditionally pnpm/lychee, uv, terraform, hadolint). Installed via + `task install`. `Brewfile.lock.json` is gitignored. **[copier]** +- **Python** (when `use_python`): `pyproject.toml` (`requires-python >=3.14`, + dev group with `black`; ansible adds `ansible-lint`/`ansible-core`), + **`.python-version`** = `3.14`, `.envrc` (direnv), managed with **uv** (`uv + sync`). **[copier]** +- **Node** (when `use_node`): managed with **pnpm**; `package.json` must declare + `"packageManager": "pnpm@…"` and `engines.node`. The template provides the + Brewfile/Taskfile/devcontainer wiring; **`package.json` itself is created during + framework scaffolding** ([manual] — see Part 2 / CHECKLIST). + +### 1.12 Versioning & releases + +- **Conventional commits drive releases.** **release-please** (when + `use_release_please`, default yes) maintains a rolling release PR; + **merging that PR** is the intentional act that cuts the tag + GitHub release + + CHANGELOG entry (`feat`→minor, `fix`→patch; pre-1.0 `feat` bumps minor; + chore/docs/ci → no release). Files: `release-please-config.json`, + `.release-please-manifest.json`. **[copier]** +- **Keep a Changelog** format; `CHANGELOG.md` is release-please-generated (and + ignored by markdownlint). **[copier]** seeds it. +- `task release:init` seeds the first `v0.1.0`; `task release:patch|minor|major` + remain a **manual override**. **Releases are never automated on a normal merge + to main.** **[manual]** to actually cut a release. +- Other root files: `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `LICENSE` + (mit/private), `.code-workspace`, `.vscode/{settings,extensions}.json`, + `.coderabbit.yaml` (CodeRabbit reviews — [manual] install the app), + `.github/PULL_REQUEST_TEMPLATE.md`, `.github/ISSUE_TEMPLATE/{bug,feature,task,research}.md`, + `.dockerignore`. **[copier]** + +--- + +## Part 2 — By project type + +Driven by the `project_type` copier answer: `general` / `web-astro` / `web-app` / +`iac` / `docs`. Two derived flags gate most behavior: `use_node` (true for +`web-astro`, `web-app`) and `use_python` (true for `iac` or when +`include_ansible`). `include_terraform` / `include_ansible` default to true for +`iac`. + +**Important:** harmon-init is a **conventions-only** template — for web/app types +it scaffolds the tooling/config but **not the framework itself**. Installing the +actual framework + stack is a [manual] CHECKLIST step. + +### 2.1 general + +The baseline. Only Part 1 universals apply. `use_node`/`use_python` both false +unless ansible/terraform opted in. + +- `test` task is a `TODO:` echo pointing to `tests/` + `docs/architecture/tests.md`. [copier] +- `validate` task echoes "No validate steps for this project type yet — see docs/CHECKLIST.md." [copier] +- `security:audit` echoes "No package manifests to audit yet." [copier] +- **[manual]:** add the primary toolchain; extend `build`/`test` accordingly. + +### 2.2 web-astro (marketing/static sites) — `use_node: true` + +Adds (all [copier] unless noted): + +- **Taskfile:** `build` (`pnpm build`), `build:preview`, `dev`, `typecheck` + (`astro check`), `lint:prettier`, `lint:eslint`, `test` (vitest if config + present), `test:e2e[:screenshot|:pdf]` (Playwright); `verify`/`ci` include + `build`. +- **prettier.config.cjs**, eslint task. **lefthook** adds prettier/eslint/ + typecheck (pre-commit) + typecheck (pre-push). +- **`lighthouserc.json`** + a **`lighthouse`** CI job in `build.yml` (Chrome + install, LHCI, PR comment). Asserts perf ≥0.7 (warn), a11y ≥0.85, BP ≥0.7, SEO + ≥0.9. +- **`docs/architecture/design-language.md`** + DESIGN.md "Visual & UX direction". +- Devcontainer forwards port **4321** (Astro dev server); `astro-build.astro-vscode` extension. +- `codeql.yml` analyzes `javascript-typescript`. +- **[manual] CHECKLIST:** `pnpm create astro@latest .`; add **Tailwind v4** + (`@tailwindcss/vite`), **zod**, **vitest**, **lucide**; move lint tooling into + `devDependencies`; switch Taskfile `npx --yes` calls to `pnpm exec`; review + `lighthouserc.json` URLs. + +### 2.3 web-app (TanStack/React apps) — `use_node: true` + +Same node tooling as web-astro **except**: + +- `typecheck` uses `tsc --noEmit` (not `astro check`). [copier] +- **No** Lighthouse job / `lighthouserc.json` (that's web-astro only). [copier] +- DESIGN.md / design-language reference **shadcn/ui** as the component set. [copier] +- **[manual] CHECKLIST:** `pnpm create @tanstack/start@latest` (TanStack Start) + or vite + react; add **Tailwind v4**, **shadcn/ui**, **zod**, **vitest**, + **lucide**; move lint tooling to `devDependencies`; switch to `pnpm exec`. + +### 2.4 iac (Terraform/Ansible) — `use_python: true` + +Adds (all [copier]): + +- **`include_terraform`** → `terraform/` skeleton (`main.tf`, `variables.tf`, + `outputs.tf`, `tfvars.env.example`); tasks `lint:terraform` (`fmt -check`), + `lint:terraform:validate`, `validate`→validate; lefthook terraform (pre-commit) + + terraform-validate (pre-push); terraform devcontainer feature; Renovate + Terraform-providers group; hashicorp/terraform extension. +- **`include_ansible`** → `ansible/` skeleton (`ansible.cfg`, `requirements.yaml`, + `inventory/ playbooks/ roles/` each `.gitkeep`); **`.ansible-lint`**; tasks + `lint:ansible`, `validate:ansible:syntax` (both guard on `ansible/site.yml` + existing); lefthook ansible-syntax (pre-push); `ANSIBLE_CONFIG` remoteEnv; + Renovate ansible regex managers; redhat.ansible extension. +- **Python toolchain** active (uv, black, `.python-version`, pyproject). +- `codeql.yml` analyzes `python` (if use_python). +- **[manual] CHECKLIST:** lay out `terraform/` and/or `ansible/site.yml` — lint + tasks activate automatically once `ansible/site.yml` exists. +- The live `harmon-infra` shows how deep the namespacing legitimately goes: + `lint:terraform:{docs,tflint,validate,security}`, + `validate:templates:*`, `deploy:ansible:{base,docker,services,…}`, + `terraform:{init,plan,apply,output}` — all still `group:action` kebab. + +### 2.5 docs (documentation/Obsidian) — neither node nor python + +Like `general` (no `build`/framework). [copier] + +- **[manual] CHECKLIST:** decide the docs toolchain (plain markdown / Obsidian + vault / static site generator). +- `obsidian_project_add` (default no) wires `util:obsidian-add` + a vault note. + +--- + +## Part 3 — Known divergences (do NOT flag these) + +Legitimately repo- or type-specific differences. An auditor should treat these as +**expected**, not drift. + +### 3.1 Conditional-by-design (driven by copier answers) + +- **No `terraform/` or `ansible/`** in non-iac repos (gated by + `include_terraform`/`include_ansible`). +- **No node tooling** (`prettier.config.cjs`, `build`/`dev` tasks, eslint/ + typecheck hooks, `lighthouse` job) in non-web repos. +- **No `pyproject.toml`/`.python-version`/`.envrc`/black** when `use_python` is + false (general/web/docs without ansible). +- **No `lighthouserc.json` / lighthouse job** outside `web-astro`. +- **No `.devcontainer/`** when `devcontainer: no` (e.g. harmon-infra was + generated with `devcontainer: false`, so it lacks the dual-profile setup — it + later added its own `.devcontainer/`). +- **No `release.yml` / release-please manifest** when `use_release_please: no` + (then releases are purely `task release:*`). +- **No `codeql.yml`** when neither node nor python. +- **No `project-automation.yml`, no org `merge_queue` rule, no + `permission-organization-projects`/`permission-members`** for personal-account + repos (`github_org == author_git_provider_username`). Org repos get all three. +- **`design-language.md` / DESIGN.md web section** only for web types; shadcn/ui + named only for `web-app`. +- **macOS-only meta** (`util:bunch-add`/`util:obsidian-add`, `.meta/`, Bunch + cask) gated by `bunch_add`/`obsidian_project_add` (default no). + +### 3.2 Tech-stack preferences (web), expected to vary by repo + +These are **defaults/recommendations**, not hard requirements — a conforming repo +picks from this palette: + +- **Astro** (web-astro), **TanStack Start / vite + react** (web-app), + **TypeScript**, **Vite**, **pnpm**, **Tailwind v4**, **zod**, **vitest**, + **lucide** (icons), **shadcn/ui** (web-app components), **alpine** (some Astro + marketing sites — e.g. sommerlawn-web uses `@astrojs/alpinejs` + `alpinejs`). +- A real web repo's `package.json` (sommerlawn-web) legitimately carries many + extra deps (markdoc, mermaid, photoswipe, remark/rehype plugins, sitemap, + astro-seo) and a large `pnpm.overrides` security-pin block + `auditConfig + .ignoreCves`. **Do not flag** project-specific dependencies, overrides, or + CVE-ignore entries — those are app decisions. + +### 3.3 Drift from older template commits (live repos lag the template) + +The live reference repos were generated from older harmon-init commits and have +since diverged. These are **template-version lag**, not violations of intent — but +a repo being *brought up to current standard* would update them: + +- **Symlink direction flipped:** live repos have `AGENTS.md -> CLAUDE.md` (and + `GEMINI.md -> CLAUDE.md`), keeping the real content in `CLAUDE.md`. The current + template makes **`AGENTS.md` canonical** with the others symlinked to it. +- **File extensions (not drift):** repos vary between `.yml` and `.yaml` + (e.g. `Taskfile.yaml` vs `Taskfile.yml`). This is by design — use whichever + extension each tool conventionally uses; never normalize or flag it. +- **Older docs layout:** live repos have flat top-level docs (`docs/security.md`, + `docs/branchProtection.md` (camelCase!), `docs/containerUpdates.md`, + `docs/dependencyUpdates.md`, `docs/architecture/architecture.md`) instead of the + current `product/ architecture/{ci-cd,security,branch-protection,tests} guides/ + runbooks/` + kebab-case filenames. They lack `docs/product/`, `docs/guides/`, + `docs/glossary.md`, `docs/conventions.md`. +- **Split CI workflows:** harmon-infra splits `build`/`security`/`validate`/ + `terraform` into separate workflow files and has extra ones (`deploy.yaml`, + `mirror-devcontainer-base.yaml`); sommerlawn-web has `-max` variants of the + claude workflows and `links-online.yml`. The current template consolidates + lint+security+build-test into `build.yml`. +- **Older copier answer keys:** harmon-infra's `.copier-answers.yml` references + now-renamed/removed questions (`github_collaboration_templates`, + `run_task_bootstrap`, `project_url`) — expected for a `_commit: v2.3.1` repo. +- **Stale `requirements.txt`** in harmon-infra alongside `pyproject.toml`/`uv.lock` + — the current template is uv/pyproject-only. + +When auditing: distinguish "**legit conditional/stack difference**" (3.1, 3.2 — +leave alone) from "**template-version lag**" (3.3 — candidate for an update toward +the current convention, but not a correctness bug).