Files
skills/init-project/SKILL.md
T
2026-05-04 11:50:49 -04:00

223 lines
9.7 KiB
Markdown

---
name: init-project
description: Scaffold a new personal project at ~/Code/<name>, push it to the user's Gitea server (didericis), and leave behind a low-dependency, text-driven repo (README, CLAUDE.md, docs/JOURNAL.md, docs/research, .claude/skills/init-entry). Interrogates the invoker for name, description, language, visibility, goals, non-goals, audience, and an optional Gitea workflow stub. Use when the user invokes /init-project or asks to "start a new project", "scaffold a repo", "spin up a new project", or similar.
---
# Scaffold a new personal project
A guided flow for creating a fresh project locally and on the user's self-hosted Gitea server. The output is a minimal, text-driven repo whose purpose, audience, goals, and decisions live in the repo itself — not in a tracker, not in chat, not in someone's head.
The invoker is an expert developer who values clarity and low dependencies. Don't add tooling, frameworks, or boilerplate beyond what's described here.
## Preflight
Before asking the invoker anything:
1. Verify both `GITEA_URL` and `GITEA_TOKEN` are set in the environment. If either is missing, stop and tell the invoker exactly which one to set. Suggest:
- `GITEA_URL` — base URL of the Gitea server (e.g. `https://gitea.example.tld`), no trailing slash.
- `GITEA_TOKEN` — a personal access token with `write:repository` scope.
2. Verify `~/Code` exists; create it if not.
3. The Gitea repo owner is always `didericis`. Do not ask.
## Step 1 — Interrogate
Gather inputs in two batches. Keep it tight.
**Batch A — free text (ask in one chat message, parse the reply):**
- **Name** — a slug. Must match `^[a-z][a-z0-9_-]*$`. Used as both the directory under `~/Code/` and the Gitea repo name. If the invoker gives something invalid, normalize and confirm.
- **One-sentence description** — used in the Gitea repo metadata and as the README's first line.
- **Goals** — what the project is trying to do. Bulleted is fine.
- **Non-goals** — what it explicitly is _not_ trying to do.
- **Audience** — who this is for (just Eric, a small group, public users).
**Batch B — structured choices (use `AskUserQuestion`):**
- Language: **Node/TypeScript**, **Python**, **None / plain text**.
- Visibility: **Private (Recommended)**, **Public**.
- Gitea workflow stub: **No (Recommended)**, **Yes — stub `.gitea/workflows/ci.yml`**.
Capture the answers verbatim where possible — especially goals and non-goals. Do not paraphrase or editorialize.
## Step 2 — Check the local path
If `~/Code/<name>` already exists, stop. Tell the invoker and let them pick another name or remove the directory themselves. Never overwrite.
## Step 3 — Scaffold the files
Create `~/Code/<name>/` and write the files below. Unix line endings, no trailing whitespace, no emojis.
### `README.md`
```
# <name>
<one-sentence description>
## Goals
<goals — verbatim>
## Non-goals
<non-goals — verbatim>
## Audience
<audience — verbatim>
```
No badges, no install section, no license blurb.
### `CLAUDE.md`
Orientation for future Claude sessions. Sections:
- **What this is** — one paragraph: name, description, why it exists.
- **Audience** — who it's for.
- **Goals / Non-goals** — verbatim from interrogation.
- **Repository layout** — describe what was actually scaffolded (don't list things that weren't created).
- **Conventions** — text-driven content; decisions, state, and stray thoughts logged in `docs/JOURNAL.md` as timestamped stream-of-thought entries (newest first, append-only, no titles, no template — tags `[name](tag://name)` go directly under the header only when a coherent theme emerges); research notes in `docs/research/`; product requirement docs in `docs/prds/`; low dependencies by default; ask before adding new tools.
- **When you're unsure** — final line: "Ask. Default to drafting in chat over editing files when the request is ambiguous."
### `.gitignore`
Always include OS junk:
```
.DS_Store
Thumbs.db
```
Plus, by language:
- **Node/TS:** `node_modules/`, `dist/`, `.env`, `.env.*`, `*.log`
- **Python:** `__pycache__/`, `*.pyc`, `.venv/`, `dist/`, `build/`, `*.egg-info/`, `.env`
- **None:** nothing extra.
### `docs/INDEX.md`
Short pointer file. Two sentences max: "Decisions and state changes are logged in `JOURNAL.md`. Research notes live in `research/`."
### `docs/prds/.gitkeep`
Empty file so the directory is tracked.
### `docs/research/.gitkeep`
Empty file so the directory is tracked.
### `docs/JOURNAL.md`
A running log of decisions, state changes, and stray thoughts. Newest entries on top, append-only, unstructured prose that captures _what I was thinking at that point in time_. Each entry: a timestamp heading (`## YYYY-MM-DD HH:MM`) followed by freeform body. No title, no template, no required sections. Tags optional — formatted as `[name](tag://name)` markdown links on the line directly under the header, only when a coherent theme has emerged.
Seed it with just the header and a one-line note describing the convention, so future-Eric (and future Claude, and the `init-entry` skill) know the format without opening this file:
```
# Journal
Append-only stream of thought. Newest entries on top. Each entry is a timestamp
followed by freeform prose. Tag entries with `[name](tag://name)` links under
the header — only when a coherent theme emerges. Otherwise just write.
```
### `.claude/skills/init-entry/SKILL.md`
Copy the global `init-entry` skill into the project so `/init-entry` is available within this repo without depending on the user's global skill set, and so the journal convention is pinned at scaffold time:
```
mkdir -p ~/Code/<name>/.claude/skills/init-entry
cp ~/.claude/skills/init-entry/SKILL.md ~/Code/<name>/.claude/skills/init-entry/SKILL.md
```
If `~/.claude/skills/init-entry/SKILL.md` doesn't exist on this machine, skip the copy and surface a warning in Step 7 — don't block the rest of the scaffold.
### Language floor (minimal, no dependencies)
- **Node/TypeScript:** `package.json` with only `name`, `version: "0.0.0"`, `description`, `private: true`. No scripts, no deps, no `tsconfig.json`. The invoker adds those when they need them.
- **Python:** `pyproject.toml` with a minimal `[project]` table — `name`, `version = "0.0.0"`, `description`. No build backend, no deps. The invoker chooses Hatch / Poetry / uv / setuptools when they're ready.
- **None:** skip.
### `.gitea/workflows/ci.yml` (only if invoker said yes)
```yaml
name: ci
on: [push, pull_request]
jobs:
hello:
runs-on: ubuntu-latest
steps:
- run: echo "hello from ${GITHUB_REPOSITORY:-$GITEA_REPOSITORY}"
```
## Step 4 — Initial commit
Run, against `~/Code/<name>`:
```
git init -b main
git add -A
git commit -m "Initial commit"
```
If `git init -b main` fails (older git), fall back to `git init && git checkout -b main` before staging.
## Step 5 — Create the Gitea repo
Build the request body with `jq -n` so quotes and special characters in the description don't break the JSON. Send:
```
curl -fsSL -X POST "$GITEA_URL/api/v1/user/repos" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg name "<name>" \
--arg desc "<description>" \
--argjson private <true|false> \
'{name: $name, description: $desc, private: $private, default_branch: "main", auto_init: false}')"
```
**Never echo, log, or write `$GITEA_TOKEN` anywhere.** Not in error messages, not in commands you describe to the user, not in any scaffolded file.
If the API call fails:
- **409 Conflict** — repo already exists on Gitea. Stop. Tell the invoker, suggest they pick a different name or delete the existing repo.
- **401 Unauthorized** — token is wrong, revoked, or lacks scope. Stop.
- **422 Unprocessable** — name violates Gitea's rules. Stop.
- Anything else — surface the response body and stop.
In every failure case the local repo is already committed and intact. Do **not** delete it.
## Step 6 — Add remote and push
Prefer SSH for the remote. Parse `ssh_url` from the API response (`jq -r '.ssh_url'`) — falls back to `clone_url` (HTTPS) only if `ssh_url` is missing or empty. Then:
```
git -C ~/Code/<name> remote add origin <ssh_url>
git -C ~/Code/<name> push -u origin main
```
SSH avoids embedding tokens in `.git/config` and matches the invoker's setup (key-based auth to the Gitea host). If the push fails on auth, surface the exact git error — do not fall back to HTTPS-with-token as a workaround, since that leaks credentials into `.git/config`.
## Step 7 — Report back
One short summary:
- Local path: `~/Code/<name>`
- Gitea URL: `html_url` from the API response.
- What was scaffolded (language floor: yes/no/which; CI stub: yes/no; init-entry skill copied: yes/skipped-with-reason).
- Suggested next step: open `CLAUDE.md` and confirm goals / non-goals / audience read correctly.
Stop there. Don't volunteer to add a license, tests, dependencies, a `src/` directory, or anything else unless the invoker asks.
## Hard rules
- **Never write the Gitea token to a file.** Not `.env`, not `CLAUDE.md`, not the `git remote` URL, not anywhere. It only lives in env.
- **Never overwrite an existing `~/Code/<name>`.** Bail with a clear error.
- **Never push if the Gitea API call failed.** Local repo stays; let the invoker decide what to do next.
- **No frameworks, no deps, no scripts, no `src/`.** This skill scaffolds a _floor_. Tooling comes later, deliberately.
- **No license file by default.** Licensing is a deliberate choice; the invoker adds it when they want it.
- **No tests, no CI** beyond the optional stub the invoker explicitly opts into.
- **Don't paraphrase goals, non-goals, or audience.** Capture what the invoker said. They're the rubric, not a draft.
- **Owner is always `didericis`.** Do not ask, do not parameterize.