--- name: cla-setup description: "Set up or refresh a repo for the cla plugin: create whatever is missing of cla.io/ without overwriting anything there, seed OpenSpec authoring rules, write the economy rules file .claude/rules/cla-economy.md (replaced whole on every run), and populate or reconcile cla.io/project-facts.md from the repo's own manifests. Changes any other existing file only on the user's yes, apart from appending one line to .gitignore. Run with /cla:cla-setup." argument-hint: "(no args — sets up the current repo)" allowed-tools: Bash, Read, Write, Edit, Grep, Glob, AskUserQuestion # Slash-command only: run deliberately, at onboarding and when facts go stale. Keeps this # description out of the always-loaded skill listing; other skills tell the user to run it. disable-model-invocation: true --- # /cla:cla-setup — set up a repo's `cla.io/` and its facts One skill for a repo's own `cla` data, in two parts: - **Part 1, structure** — create whatever is missing of the `cla.io/` tree, the run ledger, the feedback inbox and lessons log, and the OpenSpec authoring rules; report retired ledgers and keep the chains' run notes out of git, and write the economy rules file. Creates only what is missing, except that rules file, which it replaces whole. - **Part 2, facts** — read the repo's own manifests and write or reconcile `cla.io/project-facts.md`, the one home for every command, path, port, install step and env file the other skills read. Proposes first, writes only on the user's yes, then runs the staleness checker. Run it once at onboarding (marketplace install → `/cla:cla-setup` → install the `pre-push` hook) and again whenever a skill reports missing or stale facts. A re-run on a set-up repo changes nothing without a yes, except that it replaces the economy rules file with the plugin's current text and appends the run-notes line to `.gitignore` when that is missing. It replaces the former `cla-init` and `sync-context` skills, which no longer exist. The plugin itself (`SKILL.md` bodies, agents, hooks) arrives with the install and is never touched here: it is a read-only cache. ## Repo-root resolution Resolve the repo root ONCE and read and write everything relative to it: ```bash ROOT="$(git rev-parse --show-toplevel)" ``` If this errors (not inside a git working tree), stop and tell the user to run from within a git repo. ## Never-clobber contract - **Skip anything that exists — via a guarded idiom, never a bare redirect.** For every directory and seed file in Part 1 (the rules file of item 7a is the one exception, replaced whole), check existence FIRST; if present, skip it untouched (no truncate, no overwrite, no re-seed, no merge) and report `exists (skipped)`. - **The guard is load-bearing.** A bare `> "$f"` or `cat > "$f"` truncates an existing file and defeats never-clobber. ALWAYS guard with `[ -e "$f" ] ||`. Pinned idioms: - directory → `mkdir -p "$dir"` (idempotent by construction) - 0-byte ledger → `[ -e "$f" ] || : > "$f"` - seed file with content → `[ -e "$f" ] || cat > "$f" <<'EOF'` … `EOF` - **Create only what is missing.** A partly set-up repo ends fully set up with every pre-existing piece byte-for-byte unchanged, the rules file aside: a `.jsonl` with history or a filled `notes.md` is ALWAYS skipped, even though the seed content differs. - **A change to an existing file needs the user's yes, every time**; each item says where it asks. The unasked writes are item 7's appended `.gitignore` line and item 7a's rules file, which the plugin owns whole and replaces on every run. - **Overlays are optional, and this skill never creates one.** `cla.io/overlays/.md` holds a repo's skill-specific rules when it has some; a skill without one runs on its generic procedure and `cla.io/project-facts.md`. - **Report `created` vs `exists (skipped)` per target**, so a re-run is transparently a no-op. The rules file reports `created`, `replaced` or `unchanged` instead. ## Part 1 — Structure All paths relative to `$ROOT`. Items 1 to 4 create what is missing and skip what exists. Item 5 compares an existing OpenSpec config and edits it only on a yes. Item 6 deletes retired ledgers only on a yes. Item 7 appends one line to `.gitignore` unasked and untracks notes only on a yes. Item 7a replaces its rules file whole on every run. ### 1. `cla.io/` directories ```bash mkdir -p "$ROOT/cla.io/decisions" "$ROOT/cla.io/feedback" "$ROOT/cla.io/retro" "$ROOT/cla.io/lessons-learned" ``` `cla.io/decisions/` gets no seed file — an empty dir is the correct initial state (it holds ad-hoc shaped-decision `.md` files created by `shape-decision`/`multi-spec`). ### 2. Retro ledger (0-byte — an empty file is a valid empty JSONL ledger; NO `[]` or placeholder line) ```bash # The one ledger lib/log_run.py accepts; /cla:spec-to-pr-retro reads it. for f in spec-to-pr-runs; do [ -e "$ROOT/cla.io/retro/$f.jsonl" ] || : > "$ROOT/cla.io/retro/$f.jsonl" done ``` ### 3. Feedback inbox — `cla.io/feedback/notes.md` ```bash [ -e "$ROOT/cla.io/feedback/notes.md" ] || cat > "$ROOT/cla.io/feedback/notes.md" <<'EOF' # App experience notes EOF ``` ### 4. Lessons-learned log — `cla.io/lessons-learned/lessons-learned.md` ```bash [ -e "$ROOT/cla.io/lessons-learned/lessons-learned.md" ] || cat > "$ROOT/cla.io/lessons-learned/lessons-learned.md" <<'EOF' # Lessons learned EOF ``` ### 5. OpenSpec authoring rules — `openspec/config.yaml` The one path outside `cla.io/`, `.gitignore` and the rules file of item 7a this skill writes, and it is still project data. OpenSpec injects a config's `rules:` into every artifact's authoring instructions, so the block the script holds is how OpenSpec's own stock limits reach every CLA authoring run. - `openspec/` exists and neither `openspec/config.yaml` nor `openspec/config.yml` does → seed `config.yaml` and report `created`. - Either file exists → leave it **byte-identical** and compare: the script lists, under its artifact, each shipped rule the file does not carry word for word (`+`) and each earlier wording of a shipped rule it still carries (`-`), or reports the rules current. OpenSpec reads `config.yaml` first, so a new `config.yaml` would silently hide an existing `config.yml`. `openspec init` writes `config.yaml` whenever neither exists, so this is the common path. - Lines listed → show the user exactly those `+` and `-` lines and ask for a yes on that diff. Only on yes, add each `+` line under its artifact and delete each `-` line, and change nothing else: a line the script did not mark `-` is the repo's own and stays, however close it reads to a shipped rule. Never make this edit silently. - `openspec/` absent → print the block and create nothing. `openspec init` owns that directory. **OpenSpec version.** CLA relies on `openspec validate --strict` failing a MODIFIED block that drops a live scenario (verified on 1.14.1). When `openspec/` exists, run `openspec --version` and, if it is older than 1.14.1, say so in the report and recommend upgrading. Run the script, which holds the shipped `rules:` block and the earlier wordings, and report what it prints. Any non-zero exit, or output with no `openspec/...:` status line, means the config was not checked (exit 1: the script could not write or read it): say so, and never call the rules current. ```bash : "${ROOT:?ROOT unset: set it with the repo-root step at the top of this skill first}" SEED="${CLAUDE_PLUGIN_ROOT}/skills/cla-setup/scripts/seed_openspec_rules.py" [ -f "$SEED" ] || { echo "openspec/config.yaml: NOT checked, seed script not found at $SEED (plugin root unresolved?)"; exit 1; } python3 "$SEED" "$ROOT" ``` Each item in the script's `rules:` block that contains `: ` stays double-quoted, or YAML reads it as a mapping and OpenSpec drops that artifact's rules. ### 6. Retired ledgers — `cla.io/retro/` Ledgers a skill once wrote and nothing writes or reads any more. A repo onboarded earlier still holds them, and no install removes them. This block only lists the ones present: ```bash : "${ROOT:?ROOT unset: set it with the repo-root step at the top of this skill first}" for f in codify-runs commit-provenance right-model-runs multi-pr-runs multi-spec-runs \ multi-lite-runs project-review-runs lite-pr-runs shape-decision-runs feedback-runs; do if [ -e "$ROOT/cla.io/retro/$f.jsonl" ]; then echo "retired ledger: cla.io/retro/$f.jsonl"; fi done ``` - Nothing printed → say none is present. - Lines printed → show them and ask once whether to delete those files. Only on an explicit yes, `rm -- ` (exactly the listed paths, nothing else under `cla.io/retro/`), and report each `deleted`; on anything else report each `kept` and leave it. The deletion is left uncommitted, like every other change this skill makes. ### 7. Ignore the chains' run notes — `.gitignore` `/cla:multi-lite` and `/cla:multi-pr` keep their run notes as local working state that nothing commits, and refuse to start while git would see them. This adds the ignore line unless a `.gitignore` in the repo already ignores the notes (any equivalent pattern, CRLF or trailing spaces included), creating the file if needed. Only a `.gitignore` counts: this machine's global excludes and the clone's `.git/info/exclude` hide the notes here but not in any other clone. ```bash : "${ROOT:?ROOT unset: set it with the repo-root step at the top of this skill first}" line='cla.io/retro/*-run-notes-*.md' probe='cla.io/retro/multi-lite-run-notes-x.md' # The deciding pattern, as ::; global excludes switched off. match="$(git -C "$ROOT" -c core.excludesFile=/dev/null check-ignore -v --no-index "$probe")" || match='' case "${match%%:*}" in .gitignore|*/.gitignore) from_repo=1 ;; *) from_repo=0 ;; esac if [ "$from_repo" = 1 ] && git -C "$ROOT" check-ignore -q --no-index "$probe"; then echo "exists (skipped): .gitignore run-notes line" elif [ "$from_repo" = 1 ]; then # Matched but not ignored: a ! pattern un-ignores the notes on purpose. echo "un-ignored (left as is): $match" else # A last line with no newline would swallow the new one. if [ -s "$ROOT/.gitignore" ] && [ -n "$(tail -c 1 "$ROOT/.gitignore")" ]; then echo >> "$ROOT/.gitignore"; fi printf '%s\n' "$line" >> "$ROOT/.gitignore" echo "created: .gitignore run-notes line" fi ``` `un-ignored` → leave the file alone and tell the user the chains will not start until that `!` line goes. Ignoring does not untrack a notes file git already tracks; a re-run then edits it, which blocks branch switches and lets a PR pick it up. List those: ```bash : "${ROOT:?ROOT unset: set it with the repo-root step at the top of this skill first}" git -C "$ROOT" ls-files -- 'cla.io/retro/*-run-notes-*.md' | sed 's/^/tracked run notes: /' ``` - Nothing printed → say none is tracked. - Lines printed → show them and ask once whether to untrack them, warning that every other clone loses its working copies on its next pull (history keeps them). Only on an explicit yes, `git -C "$ROOT" rm -q --cached -- ` (exactly the listed paths; the files stay on disk), and report each `untracked`; on anything else report each `kept tracked`. Left uncommitted, like item 6. ### 7a. The economy rules file — `.claude/rules/cla-economy.md` An agent working in this repo outside any skill reads the repo's own `CLAUDE.md` and `.claude/rules/*.md` and nothing from the plugin. A rules file with no `paths:` frontmatter loads at launch with the same priority as `.claude/CLAUDE.md`, so this file gets the cost rules to that agent. The plugin owns the file whole: it is compared on every run and replaced with no prompt when it differs, so a hand edit is replaced and the report says so (the repo's own rules go in its `CLAUDE.md`). The file is usually committed in the repo, so an upgrade that changes it shows as an ordinary diff; if `.claude/` is ignored it stays local. This skill never touches `CLAUDE.md`. The text is `${CLAUDE_PLUGIN_ROOT}/skills/_shared/references/economy-rules-file.md` with two sources inlined at its markers: `orchestration-economy.md` whole, and the span of `model-routing.md` between its `review-sizing` begin and end comments. Every source is read through `tr -d '\r'`, so a CRLF checkout and an LF one write the same bytes. The block prints `created`, `replaced` (existing content differed, and a second line says local edits were overwritten) or `unchanged`, and writes only when the text differs. Before writing it requires every source to be present and non-empty, the begin and end comments to occur exactly once each and in that order, and each include marker in the frame to occur exactly once; it renders to a temporary file and moves that into place, so a failed write leaves the existing file untouched. Any failure prints `NOT written` with the reason and stops the block: report it, then carry on with item 8 (Part 2). A failure here never stops the skill. The harness substitutes the literal plugin path for the `CLAUDE_PLUGIN_ROOT` placeholder in this text when the skill loads; it is not a shell variable. If the block prints an unsubstituted or empty root, it aborts without writing. ```bash : "${ROOT:?ROOT unset: set it with the repo-root step at the top of this skill first}" PLUGIN="${CLAUDE_PLUGIN_ROOT}" [ -n "$PLUGIN" ] || { echo ".claude/rules/cla-economy.md: NOT written, plugin root unresolved"; exit 1; } REF="$PLUGIN/skills/_shared/references" RULES_FILE="$ROOT/.claude/rules/cla-economy.md" fail() { echo ".claude/rules/cla-economy.md: NOT written, $1"; exit 1; } FRAME="$(tr -d '\r' < "$REF/economy-rules-file.md")" COST="$(tr -d '\r' < "$REF/orchestration-economy.md")" MODEL="$(tr -d '\r' < "$REF/model-routing.md")" if [ -z "$FRAME" ] || [ -z "$COST" ] || [ -z "$MODEL" ]; then fail "a source is missing or empty under $REF" fi BEGIN='' END='' INC_COST='' INC_SIZING='' [ "$(printf '%s\n' "$MODEL" | grep -cxF "$BEGIN")" = 1 ] && [ "$(printf '%s\n' "$MODEL" | grep -cxF "$END")" = 1 ] || fail "model-routing.md must hold the review-sizing begin and end comments exactly once each" LB="$(printf '%s\n' "$MODEL" | grep -nxF "$BEGIN" | cut -d: -f1)" LE="$(printf '%s\n' "$MODEL" | grep -nxF "$END" | cut -d: -f1)" [ "$LB" -lt "$LE" ] || fail "model-routing.md has the review-sizing end comment before the begin comment" [ "$((LE - LB))" -gt 1 ] || fail "the review-sizing span in model-routing.md is empty" SIZING="$(printf '%s\n' "$MODEL" | sed -n "$((LB + 1)),$((LE - 1))p")" [ -n "$SIZING" ] || fail "the review-sizing span in model-routing.md is empty" [ "$(printf '%s\n' "$FRAME" | grep -cxF "$INC_COST")" = 1 ] && [ "$(printf '%s\n' "$FRAME" | grep -cxF "$INC_SIZING")" = 1 ] && [ "$(printf '%s\n' "$FRAME" | grep -cF '