# claude-playground A tiny zero-dependency Node.js todo CLI, used as a sandbox for trying Claude Code features. ## Commands - `npm install` — once per clone (no dependencies): installs the git hooks that block commits and pushes to `main`. If it fails in the sandbox, ask the human to run it. - `npm test` — run the test suite (built-in `node:test`) - `npm start -- add "something"` / `npm start -- ls` — run the CLI (data in `todos.json`, override with `TODO_FILE`) ## Conventions - ES modules, Node >= 20, no third-party dependencies. - `src/todo.js` holds pure functions; keep I/O in `load`/`save` and `src/cli.js`. - Every behavior change gets a test in `test/`. - Commit messages: short imperative subject line. ## Workflow (required) Never commit, merge, or push to `main`. Only a human (Christian) merges into `main`, in Gitea. The git hooks installed by `npm install` and Gitea branch protection enforce this (see "Protecting `main`" in the README). Never get around them: no `--no-verify`, no changing `core.hooksPath`, no editing `.git/`. 1. **Branch.** Run `git fetch origin` on its own, then `git switch --no-track -c / origin/main` (e.g. `fix/duplicate-ids`). 2. **Commit, test, push.** `npm test` must pass. Check that `git branch --show-current` isn't `main`, then run `git push -u origin HEAD`. 3. **Open the PR**, once per branch (see "Gitea API"). 4. **Review** with `/code-review medium`. 5. **Triage.** The review lists unverified candidates, and it will always find some. Check each one against the code. A finding **blocks** only if it is a correctness bug in this PR's change that you have confirmed, ideally with a test that fails. Everything else is a **note**: cleanups, judgement calls, hardening ideas and unconfirmed findings. 6. **Comment** on the PR: - First line: **Needs fixes** if anything blocks, otherwise **Approved**. - Second line: `_Automated self-review by Claude; not an independent review._` - Then the blocking findings, then the notes, then the findings you rejected, each with the reason. 7. **At most one fix round.** If the PR needs fixes, fix only the blocking findings, each with a test. Push, review once more, and post the verdict from steps 5–6. Then stop, whatever that second review says. Findings about the fixes themselves go to the human as notes; don't fix them. 8. **Hand off.** Never merge, even if the PR is approved. Tell the human it's ready, with the verdict and any notes. Don't widen scope beyond the PR's purpose to satisfy a review. If a fix would add significant new code (a new mechanism, not a correction), list it as a note for the human instead. Bring in the latest `main` only when the PR conflicts or the human asks: 1. Run `git fetch origin`. 2. Run `git merge --no-ff --no-commit origin/main`. 3. If it says "Already up to date", stop here. Otherwise resolve any conflicts and run `npm test`. 4. Run `git commit --no-edit`. Run each as its own command, and never rebase or force-push. ### Gitea API No `gh`/`tea`: use `curl` with `$GITEA_TOKEN`. One-time human setup in the git-ignored `.claude/settings.local.json`: set `env.GITEA_TOKEN`, and add `code.westerkam.org` to `sandbox.network.allowedDomains`. Then restart the session (`claude --continue`); settings load only at start. If the token is unset, stop and ask the human. Rules for every API call: - Make each call one self-contained command, because shell state doesn't carry over between commands. - Put titles and bodies in files in `$TMPDIR` (never in the repo) and pass them with `--rawfile`, never inline in double quotes, where backticks and `$(…)` would run. - The token goes in a private header file, so it doesn't show up in the process list. - `pipefail`, `curl -f` and the `jq` checks make every failure exit non-zero. ```sh # Open a PR for the current branch (once). Write $TMPDIR/pr-title.txt and $TMPDIR/pr-body.md first. set -o pipefail; API=https://code.westerkam.org/api/v1/repos/christian/claude-playground (umask 077; printf 'Authorization: token %s\n' "$GITEA_TOKEN" > "$TMPDIR/gitea-auth") jq -n --arg head "$(git branch --show-current)" --rawfile title "$TMPDIR/pr-title.txt" --rawfile body "$TMPDIR/pr-body.md" \ '{head:$head, base:"main", title:($title|rtrimstr("\n")), body:$body}' \ | curl -fsS -X POST -H @"$TMPDIR/gitea-auth" -H "Content-Type: application/json" -d @- "$API/pulls" \ | jq -er 'if .number then "#\(.number) \(.html_url)" else error("no PR number in response") end' # Comment on PR . Write $TMPDIR/comment.md first. set -o pipefail; API=https://code.westerkam.org/api/v1/repos/christian/claude-playground (umask 077; printf 'Authorization: token %s\n' "$GITEA_TOKEN" > "$TMPDIR/gitea-auth") jq -n --rawfile body "$TMPDIR/comment.md" '{body:$body}' \ | curl -fsS -X POST -H @"$TMPDIR/gitea-auth" -H "Content-Type: application/json" -d @- "$API/issues//comments" \ | jq -er .html_url ``` Commands listed under `sandbox.excludedCommands` in `.claude/settings.json` (`git fetch`, `git commit`, `git push` and others) run outside the sandbox only when they are run standalone: no `cd … &&` before them and nothing chained after. They need that to reach the remote and to sign commits. ## Remote Gitea at code.westerkam.org — `origin` = `git@code.westerkam.org:christian/claude-playground.git`.