Scaffold: agent starter kit (seeded from Ebb)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
- [Orchestrator never codes](orchestrator-never-codes.md) — every repo change via an Opus agent; a slip is kept, never reverted
|
||||
- [Subagents are Opus only](subagents-opus-only.md) — smaller models only for owner-requested read-only scans
|
||||
- [Relay HUMAN steps in chat](relay-human-steps-in-chat.md) — exact headings and button labels, expected result; owner reads no md files
|
||||
- [Pull numbers yourself](pull-numbers-yourself.md) — adb pull / run-as / logcat and a summary script; owner reads nothing off the phone
|
||||
- [Do installs yourself](do-installs-yourself.md) — package check and installDebug without asking when the phone is attached
|
||||
- [Ask owner to tap](ask-owner-to-tap.md) — no adb force-runs or broadcasts on the real-data app
|
||||
- [Phone install safety](phone-install-safety.md) — confirm ids; never clear real data; one agent on the phone
|
||||
- [Local builds only](local-builds-only.md) — no CI; openjdk@25 via PATH; adb full path
|
||||
- [Stay inside the project folder](stay-inside-project-folder.md) — ask once for anything outside the repo
|
||||
- [Gradle disk hygiene](gradle-disk-hygiene.md) — caches may be deleted; init script at 7 days; clean-builds.sh
|
||||
- [Owner-only files](human-todo-owner-only.md) — never read the owner's personal todo
|
||||
- [One source of truth](one-source-of-truth.md) — one rules file, one handoff, one decisions log
|
||||
- [Landing protocol](landing-protocol.md) — ff-only, gate, install, remove worktree; numbering in briefs
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
name: ask-owner-to-tap
|
||||
description: Trigger on-phone actions through the owner's taps, never through adb force-runs, broadcasts or am start
|
||||
metadata:
|
||||
type: feedback
|
||||
---
|
||||
|
||||
Carried over from Ebb: permissions, sign-ins, vendor prompts and anything that changes app state on the owner's real-data install are the owner's taps. Agents may read (pull, logcat) but not drive the app.
|
||||
|
||||
**Why:** forced runs bypassed permission prompts and produced states the owner could not reproduce.
|
||||
|
||||
**How to apply:** write the step with exact labels and the expected result and wait. See [[relay-human-steps-in-chat]].
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
name: do-installs-yourself
|
||||
description: Run adb checks, installs and pulls yourself when the phone is attached; HUMAN steps are for taps only
|
||||
metadata:
|
||||
type: feedback
|
||||
---
|
||||
|
||||
Owner (Ebb, 2026-10-17): "these things you can do yourself, don't ask me to do things you can do."
|
||||
|
||||
**Why:** asking the owner to type adb or Gradle commands wastes their time.
|
||||
|
||||
**How to apply:** when the phone is attached: `adb shell pm list packages | grep <id>`, then `installDebug` from main without asking; keep [[phone-install-safety]]. If not attached, say so once and install when it appears. See [[ask-owner-to-tap]].
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: gradle-disk-hygiene
|
||||
description: Parallel agent builds fill the disk; owner allowed deleting ~/.gradle caches; init script keeps caches at 7 days; run tools/clean-builds.sh after removing a worktree
|
||||
metadata:
|
||||
type: feedback
|
||||
---
|
||||
|
||||
Ebb, 2026-10-17: disk hit zero with three agents building (11 GB stale Gradle version cache, 7 GB transforms, local build cache). Owner: "You're allowed to delete the gradle cache."
|
||||
|
||||
**How to apply:** check `df -h /System/Volumes/Data` before spawning parallel builders; under ~5 GB free, delete `~/.gradle/caches/build-cache-1` and old `~/.gradle/caches/<version>` dirs, then `tools/clean-builds.sh`. An agent that hit ENOSPC is resumed, not respawned; its commits survive. The user-level init script `~/.gradle/init.d/cache-retention.gradle.kts` already exists on this MacBook.
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
name: human-todo-owner-only
|
||||
description: A file the owner keeps for themselves (e.g. human-todo.md) is never read or edited by agents; status lives in docs/HANDOFF.md
|
||||
metadata:
|
||||
type: feedback
|
||||
---
|
||||
|
||||
Carried over from Ebb. If such a file exists here, treat it the same way. See [[relay-human-steps-in-chat]].
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: landing-protocol
|
||||
description: How the orchestrator lands an agent branch: ff-only merge, gate on main, install, remove worktree, delete branch; agents never land; numbering handed out in briefs
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
Pattern that worked on Ebb (2026-10): agent works in `.claude/worktrees/agent-<id>`, starts with `git reset --hard main`, copies git-ignored `local.properties` and vendor binaries, builds only via `tools/gradle-serial.sh`, commits, rebases on main, gates, reports (branch, head, gate lines, one line per item, owner steps). Orchestrator: `git merge --ff-only`, gate on main, `installDebug` if the phone is attached, `git worktree remove --force --force` (fallback `rm -rf` + `git worktree prune`), `git branch -d`. Bulk `git rm`/`git branch -D` can be blocked by the permission classifier: ask the owner to run it with `!`.
|
||||
|
||||
**How to apply:** two parallel agents get disjoint decision/spec numbers in their briefs; the second to land renumbers on rebase. Tell a running agent when main moves. See [[orchestrator-never-codes]], [[gradle-disk-hygiene]].
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: local-builds-only
|
||||
description: Everything builds locally on the MacBook; no CI, no remote build service; JDK via Homebrew openjdk@25, adb at /opt/homebrew/share/android-commandlinetools/platform-tools/adb
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
Carried over from Ebb (2026-10). Gradle runs as `PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH tools/gradle-serial.sh …`; the shell's `java` is a macOS stub. Never set JAVA_HOME. `adb` is not on PATH.
|
||||
|
||||
**How to apply:** do not add CI or hosted runners; report real gate lines from local runs only.
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: one-source-of-truth
|
||||
description: Owner wants exactly one working-rules file, one handoff, one decisions log; delete superseded copies the same day
|
||||
metadata:
|
||||
type: feedback
|
||||
---
|
||||
|
||||
Owner (Ebb, 2026-10-17): "Remove anything old (agent.md etc) we should only have one source of truth."
|
||||
|
||||
**How to apply:** when a document is superseded, fold it into the survivor and delete it in the same commit; repoint references with `git grep`. Stale status lines in reports get a dated new line with the old one struck through.
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
name: orchestrator-never-codes
|
||||
description: The orchestrator session never writes code or repo changes itself; every change, however small, goes through an Opus subagent; a slip is kept and handed over, never reverted
|
||||
metadata:
|
||||
type: feedback
|
||||
---
|
||||
|
||||
Owner rule (Ebb, 2026-10-17): "you are not allowed to code, only opus subagents are." And: "if you slip up, no need to undo the work. that is just stupid and wasteful."
|
||||
|
||||
**Why:** implementation, review and verification run on Opus agents with their own worktree and gate; the orchestrator keeps the overview, lands, installs and talks to the owner.
|
||||
|
||||
**How to apply:** for any repository change, including docs, decisions and config comments, spawn an Opus agent with a brief. The orchestrator may read, run gates, fast-forward main, run adb and answer questions. A change started by mistake is kept and handed to an agent to own. See [[subagents-opus-only]], [[do-installs-yourself]].
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: phone-install-safety
|
||||
description: Confirm package ids before installing; never clear or uninstall an install that holds the owner's real data; one agent on the phone at a time
|
||||
metadata:
|
||||
type: feedback
|
||||
---
|
||||
|
||||
Carried over from Ebb, where a data-collecting spike app lived on the same phone and a wrong id would have cost days.
|
||||
|
||||
**How to apply:** `adb shell pm list packages | grep <id>` before the first install of a session; installs keep data; "clear data" or uninstall of a real-data build needs the owner's yes; the brief names which agent may touch the phone. See [[do-installs-yourself]].
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
name: pull-numbers-yourself
|
||||
description: Never ask the owner to read numbers, tables or logs off the phone; pull them over USB (adb pull, run-as cat, logcat -d) and summarise with a script, aggregates only
|
||||
metadata:
|
||||
type: feedback
|
||||
---
|
||||
|
||||
Owner (Ebb, 2026-10-17), when asked to read timing tables aloud: "are you kidding me? Either you read it or make it so that I can export a report… very disappointed honestly."
|
||||
|
||||
**Why:** the owner's phone time is for taps and yes/no observations only.
|
||||
|
||||
**How to apply:** every debug feature writes a file under the app's external files dir; the orchestrator pulls it (`adb pull /sdcard/Android/data/<pkg>/files/...`, `adb shell run-as <pkg> cat files/...`, `adb logcat -d -s <tag>`) into the scratchpad and summarises with a script (medians, counts; never content). See [[relay-human-steps-in-chat]], [[do-installs-yourself]].
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
name: relay-human-steps-in-chat
|
||||
description: The owner never reads HUMAN steps in md files; every owner action is spelled out in the chat reply with exact section headings and button labels and the expected result
|
||||
metadata:
|
||||
type: feedback
|
||||
---
|
||||
|
||||
Owner (Ebb, 2026-10-17): "never read these [md files], you need to always tell me clearly what you expect me to do." And on an imprecise label: "your instructions must be much more clear. They must be exactly what I should press."
|
||||
|
||||
**Why:** a step that lives only in a report is never done, and work waits on it silently; a vague label makes the owner do the wrong thing.
|
||||
|
||||
**How to apply:** numbered list in the reply, each step = where (heading verbatim) + what to press (label verbatim) + expected result. Read the strings or composables first when unsure. Keep a `docs/debug-screen.md` map of every heading and button, kept true by a test. See [[pull-numbers-yourself]], [[ask-owner-to-tap]].
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: stay-inside-project-folder
|
||||
description: Nothing outside the repository without an explicit allowance; no unasked side tasks
|
||||
metadata:
|
||||
type: feedback
|
||||
---
|
||||
|
||||
Carried over from Ebb. Exceptions the owner granted there: reading `~/.android/debug.keystore` for a signature hash (asked first), writing `~/.gradle/init.d/cache-retention.gradle.kts`, deleting `~/.gradle/caches` when the disk was full.
|
||||
|
||||
**How to apply:** ask once, in one line, when a task needs a file outside the repo; keep the allowance scoped to that file.
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
name: subagents-opus-only
|
||||
description: Subagents are Opus; Haiku or Sonnet only for a read-only scan the owner explicitly asks for that way
|
||||
metadata:
|
||||
type: feedback
|
||||
---
|
||||
|
||||
Owner rule carried over from Ebb: every implementation, review and verification agent is Opus. One task per agent, each in its own worktree, briefed with goal, files to read, established facts, numbers it may use, setup, gate and report format.
|
||||
|
||||
**Why:** smaller models produced work that had to be redone; the owner would rather pay for one correct pass.
|
||||
|
||||
**How to apply:** `model: opus` on every Agent call unless the owner names another model for a specific read-only job. See [[orchestrator-never-codes]].
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# Build output
|
||||
build/
|
||||
.gradle/
|
||||
.kotlin/
|
||||
*.iml
|
||||
.idea/
|
||||
local.properties
|
||||
# Secrets, keys, vendor binaries, models, personal data: never committed
|
||||
*.jks
|
||||
*.keystore
|
||||
*.p12
|
||||
*.aar
|
||||
models/
|
||||
export/
|
||||
# Agent worktrees live here (git ignores the folder; `git worktree` tracks them in .git)
|
||||
.claude/worktrees/
|
||||
# macOS
|
||||
.DS_Store
|
||||
@@ -0,0 +1,139 @@
|
||||
# CLAUDE.md
|
||||
|
||||
Operating guide for AI agents and humans working on **Kvitt**. This is the only working-rules file; it says how to work here, not what the product is. For the product, read `SPEC.md`. It was seeded on 2026-10-17 from the Ebb project's rules after a year of running Opus agents in parallel; keep what fits, delete what does not, but keep one file.
|
||||
|
||||
> **Fill in before the first plan:** the one-paragraph product description, the module layout under "Repository layout", the application ids under "The phone" (if this is an Android app), and the fast checks in `tools/git-hooks/pre-commit`.
|
||||
|
||||
---
|
||||
|
||||
## SOURCE OF TRUTH
|
||||
|
||||
* **`SPEC.md` wins.** If a plan, this file or existing code disagrees with the spec, the spec is right.
|
||||
* A revised decision updates the spec **in the same change**, with an entry in `docs/decisions.md` (numbered D-001…; never deleted, revised in place with a pointer).
|
||||
* Each `PLAN-*.md` lists the spec sections to read first. Read them before writing anything.
|
||||
* Never encode a threshold, limit or rule that is not in the spec. Ask, or leave a `TODO(spec)`; a wrong number is worse than a missing one.
|
||||
* One handoff: `docs/HANDOFF.md`, rewritten in place. One decisions log. One working-rules file (this one). Delete superseded copies the same day; never keep two sources of truth.
|
||||
|
||||
---
|
||||
|
||||
## REPOSITORY LAYOUT
|
||||
|
||||
```
|
||||
SPEC.md, PLAN-*.md what to build; prompts/ holds a ready-to-paste start prompt per plan and the orchestrator prompt
|
||||
docs/HANDOFF.md state of the work and how it is run
|
||||
docs/decisions.md decisions that changed or refined the spec
|
||||
docs/spikes/, docs/validation/ what the device and the data showed (reports are deliverables)
|
||||
tools/ gradle-serial.sh, clean-builds.sh, install-git-hooks.sh, dependency-report/latest.py
|
||||
(root) the app: one Gradle project, modules as in SPEC
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ORCHESTRATION: HOW WORK IS RUN
|
||||
|
||||
The owner talks to one **orchestrator** session. The orchestrator plans, briefs, lands, gates, installs and answers; it **never writes code or repository changes itself**, however small (docs and config included). Every change goes through an **Opus** implementation agent. If the orchestrator slips and edits something, it keeps the work and hands it to an agent to own; it never reverts or redoes it.
|
||||
|
||||
* **One Opus agent per task, in its own git worktree** (`.claude/worktrees/agent-<id>`, branch of the same name). The agent's first step is `git reset --hard main`, then copying `local.properties` (and any git-ignored vendor binaries) from the main checkout. It commits on its branch, rebases on `main`, runs the gate, and reports; it never lands, pushes or removes its worktree.
|
||||
* **The orchestrator lands:** `git merge --ff-only <branch>` in the main checkout, re-runs the gate on `main`, installs on the phone if attached, removes the worktree (`git worktree remove --force --force`, falling back to `rm -rf` + `git worktree prune`) and deletes the branch. No merge commits. Never rebase `main`.
|
||||
* **Briefs are the whole context an agent has.** A brief states: the goal, what to read first (file paths), what is already established, the exact numbers it may use (next free decision, spec version, schema), the setup steps, the gate, what to report. If the orchestrator cannot write a clear brief, it does not understand the task yet.
|
||||
* **Agents' reports are not shown to the owner.** The orchestrator extracts the decision: one line per finding, fixed or not, and relays every owner step in chat.
|
||||
* **Subagents are Opus.** Haiku or Sonnet only for a read-only scan the owner explicitly asks for that way.
|
||||
* **Numbering** (decisions, spec versions, schema versions, contract requests) is handed out by the orchestrator in the brief. Two parallel agents get disjoint ranges; the second to land renumbers to be contiguous on rebase.
|
||||
* **Parallel agents** each have their own worktree, build through `tools/gradle-serial.sh` (one Gradle build at a time on this machine), and take turns on the phone: **one agent on the phone at a time**, named in the brief.
|
||||
|
||||
---
|
||||
|
||||
## THE OWNER
|
||||
|
||||
* **Owner steps go in the chat reply**, never only in a file. Exact section headings and button labels as they appear on screen, in order, with the expected result. Taps and yes/no observations only.
|
||||
* **Never ask the owner to read numbers, tables or logs off the phone.** Pull them over USB (`adb pull` of the app's external files, `adb shell run-as <package> cat` for internal files, `adb logcat -d`), summarise with a script, aggregates only. Every debug feature writes a file that can be pulled.
|
||||
* **Do yourself what you can do:** package checks, installs, pulls, gates. HUMAN steps are for taps, permissions, sign-ins and looking at a screen.
|
||||
* Confirm before anything destructive on the owner's data. "Delete everything" on a real-data install waits for the owner's yes.
|
||||
* The owner's own files (for example a personal todo) are not read or edited by agents unless asked.
|
||||
* When the owner has answered a question, record the answer in the decisions log or the handoff the same day; stale open questions cost the owner's time twice.
|
||||
|
||||
---
|
||||
|
||||
## THE PHONE (if this is a device app)
|
||||
|
||||
| App | Application ID |
|
||||
|---|---|
|
||||
| Real app, release | `<id>` |
|
||||
| Real app, debug | `<id>.debug` |
|
||||
| Instrumented-test APKs | `<id>.<module>.test` |
|
||||
|
||||
* `adb` is at `/opt/homebrew/share/android-commandlinetools/platform-tools/adb` on this MacBook; it is not on the shell PATH.
|
||||
* Use `adb` **only for this project's apps**. Do not browse, copy or change anything else on the device.
|
||||
* Before the first install in a session, `adb shell pm list packages | grep <id>` and confirm the ids above still hold.
|
||||
* Never clear or uninstall a build that holds the owner's real data without asking. Instrumented tests install their own APK and never touch the real app's data.
|
||||
* Never log personal content (values, names, titles, notes), in debug builds either. Logs, screenshots and database files from the device stay out of git; committed reports carry aggregates.
|
||||
|
||||
---
|
||||
|
||||
## BUILD AND VERIFY
|
||||
|
||||
Everything builds **locally on this MacBook**. No hosted CI, no remote build service.
|
||||
|
||||
**JDK.** Gradle runs on JDK 25 (current LTS): `gradle/gradle-daemon-jvm.properties` asks for 25 and `gradle.properties` points Gradle at Homebrew's `openjdk@25` (auto-download off). The shell's `java` is a macOS stub or a newer non-LTS: run Gradle as `PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH tools/gradle-serial.sh <tasks>` and **never set `JAVA_HOME`**. Do not install another JDK or change the pin without asking.
|
||||
|
||||
**Android SDK.** `local.properties` must contain `sdk.dir` (`/opt/homebrew/share/android-commandlinetools` on this MacBook). Add the line if missing; never overwrite the file, it also holds client ids and tokens; never commit it.
|
||||
|
||||
```bash
|
||||
tools/gradle-serial.sh check # THE gate: build, unit tests, lint, architecture rules, screenshot tests
|
||||
tools/gradle-serial.sh installDebug # install the debug app on the attached phone
|
||||
tools/install-git-hooks.sh # once per checkout
|
||||
tools/clean-builds.sh [--dry-run] # delete build output in every checkout and worktree (disk)
|
||||
python3 tools/dependency-report/latest.py # version drift of gradle/libs.versions.toml
|
||||
```
|
||||
|
||||
* **`check` is the single gate.** Every check a plan asks for must be reachable from it. Report gates with the real result lines (BUILD line, actionable tasks, test counts from fresh XML only). Never report a check as passing that you did not run in this session.
|
||||
* **One Gradle build at a time.** Parallel builds exhaust memory. Always `tools/gradle-serial.sh`; it waits on a lock in `/tmp` and then runs the wrapper of your worktree.
|
||||
* **Disk.** Three agents plus the main checkout can fill a disk. `~/.gradle/init.d/cache-retention.gradle.kts` (user-level, outside the repo) keeps `~/.gradle/caches` at 7 days with cleanup on every build; Gradle 9 accepts build-cache retention only there. Run `tools/clean-builds.sh` after removing a worktree or when free space is under about 5 GB. Deleting `~/.gradle/caches/build-cache-1` and old `~/.gradle/caches/<version>` folders is allowed; the next build is a slow full one. An agent that hits "no space left on device" loses only uncommitted work: free space and resume it, do not respawn.
|
||||
* **Version drift:** run the dependency report at the start of every plan. A library more than one stable minor behind gets a line in the plan's report; a toolchain component more than one line behind triggers an upgrade plan first.
|
||||
* **Screenshots.** Goldens are committed; `check` fails when a render differs. Re-record only for an intended change and **open every changed PNG** before committing. Code that compiles is not evidence of a good screen.
|
||||
* **Every schema change gets a migration and a migration test.** User decisions stored in the database are not derived data and must survive every migration.
|
||||
|
||||
---
|
||||
|
||||
## ARCHITECTURE RULES
|
||||
|
||||
Enforced by an architecture check inside `check` where possible. Fill in from SPEC; the shape that worked:
|
||||
|
||||
* **Layering:** `feature → core:model`, `domain → core:model`, `data → core:model`; only `:app` sees everything. Pure modules (`:core:model`, `:domain:*`) import only `kotlin.*`, `kotlinx.*` and the project's own packages: no `java.time`, no `java.io`, no Android.
|
||||
* **Vendor SDKs live in one adapter module each** and map to the project's own records inside it. Sources are plugins behind a registry; nothing names a vendor outside its adapter. Source ids are open strings, never enums.
|
||||
* **The UI contract is the contract:** features consume `*State` classes through repository interfaces; a contract change is made in the model, the fakes and the real repositories in the same change.
|
||||
* **Fakes for every repository**, driven by scenarios, so screens render and screenshot-test without a device. Fakes never reach a release build.
|
||||
* **Fast-moving APIs:** confirm current usage from the official documentation at implementation time. Do not rely on memory.
|
||||
* **Licences first:** no vendor binary and no model file is committed, bundled or downloaded by a build before the owner has cleared its licence.
|
||||
|
||||
---
|
||||
|
||||
## ANTI-PATTERNS
|
||||
|
||||
* **No silencing:** no `!!`, no `@Suppress` / `@SuppressLint`, no lint-baseline additions to make a check pass. Fix the type or the code.
|
||||
* **No mocking libraries** (Mockito, MockK). Inject the dependency and use a typed fake or a recorded fixture.
|
||||
* **No empty `catch`.** Handle or log explicitly; never swallow `CancellationException`.
|
||||
* **Testing integrity:** never delete, disable or loosen a failing test to get green. Fix the code, or change the test with a stated reason.
|
||||
* **No hard-coded colours, sizes or strings in feature modules.** Tokens from one design file; strings from resources, every supported language.
|
||||
* **No placeholder text** in previews or screenshots.
|
||||
* **No assumptions:** read a file before modifying or citing it.
|
||||
* **Never commit:** `local.properties`, keystores, `.aar` or other vendor binaries, model files, client ids, tokens, personal data.
|
||||
|
||||
---
|
||||
|
||||
## EXECUTION PROTOCOLS
|
||||
|
||||
* **Read before edit:** read the whole target file first. After editing, check for duplicated lines and intact imports.
|
||||
* **Stay in scope:** each plan has an "Out of scope" list. Respect it; note ideas in the plan's report instead of building them.
|
||||
* **Stay inside the repository.** Nothing outside it without an explicit allowance from the owner (the user-level Gradle init script was one).
|
||||
* **Stop and ask** when: a step could disturb the owner's real data or another app on the phone, the build needs a tool the owner has not installed, a licence is not cleared, or the spec is ambiguous in a way that changes a decision.
|
||||
* **Always commit** finished work once the gate is green. Never commit mid-change or over a failing check. Never commit on `main`; one branch per task.
|
||||
* **Push and pull requests need an explicit ask.**
|
||||
* **Reports are deliverables:** `docs/spikes/`, `docs/validation/`, complete, aggregates only, with a "Results (owner's phone)" section when a measurement is still to come.
|
||||
* **Navigation:** `git ls-files` to discover, `git grep` to search; never `ls -R`, never `find` without `-maxdepth`.
|
||||
|
||||
---
|
||||
|
||||
## OUTPUTS (orchestrator replies)
|
||||
|
||||
**Default reply: 1–5 lines.** Lead with the answer; a yes/no question gets yes or no on line one. One line per finding. Never re-narrate a diff or an agent report; extract the decision. No "want me to X?" when X is obvious. May run long: owner steps (exact labels, expected results), raw failure output, an open trade-off that needs the owner's decision, and the reports a plan asks for.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Kvitt agent starter kit (seeded 2026-10-17 from the Ebb project)
|
||||
|
||||
What is here and why:
|
||||
|
||||
- `CLAUDE.md`: the working rules (orchestrator + Opus agents, worktrees, serial builds, gate, phone, owner steps). Fill in the marked blanks before the first plan.
|
||||
- `docs/HANDOFF.md`, `docs/decisions.md`: templates for the one handoff and the one decisions log.
|
||||
- `prompts/ORCHESTRATOR-prompt.md`: paste this to start an orchestrator session.
|
||||
- `tools/gradle-serial.sh`: one Gradle build at a time across all worktrees (lock in /tmp/kvitt-gradle.lock).
|
||||
- `tools/clean-builds.sh`: delete build output in every checkout and worktree; `--dry-run` lists.
|
||||
- `tools/install-git-hooks.sh` + `tools/git-hooks/pre-commit`: secrets guard + fast tests; adjust the Gradle task.
|
||||
- `tools/dependency-report/latest.py`: version drift of `gradle/libs.versions.toml` (needs that file).
|
||||
- `gradle.properties`, `gradle/gradle-daemon-jvm.properties`: the JDK 25 pin and caching flags. Merge into the generated project's files.
|
||||
- `.gitignore`: starter list of what never reaches git.
|
||||
- `.claude/memory-seed/`: the agent memories generalised from Ebb. They are already installed in
|
||||
`~/.claude/projects/-Users-jhag-projects-kvitt/memory/` (the folder Claude Code loads for `~/projects/kvitt`),
|
||||
so an orchestrator opened in this folder starts with them. This copy is for reference and for seeding another machine.
|
||||
- Outside the repo, already in place on this MacBook: `~/.gradle/init.d/cache-retention.gradle.kts` (Gradle cache cleanup, 7 days).
|
||||
|
||||
Not copied on purpose: Ebb's SPEC, plans, architecture-check build-logic and product memories. Write Kvitt's own.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Handoff: state of the work
|
||||
|
||||
Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.
|
||||
|
||||
## 1. Where things stand
|
||||
- `main` at <hash>, last gate: <date, result line>.
|
||||
- Installed on the owner's phone: <package> from <hash>.
|
||||
|
||||
## 2. Open work (one line each, with who owns it: agent or owner)
|
||||
|
||||
## 3. Owner questions (asked, unanswered)
|
||||
|
||||
## 4. Paused (waiting on something outside the repo)
|
||||
|
||||
## 5. Numbering
|
||||
Next free: D-001, SPEC v0.1, schema 1.
|
||||
|
||||
## 6. How the work is run
|
||||
See CLAUDE.md › Orchestration. HUMAN steps in reports are relayed in chat by the orchestrator; the owner does not
|
||||
read them in files.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Decisions
|
||||
|
||||
One entry per decision that changes or refines SPEC.md. Numbered, dated, never deleted: a revised decision gets a new
|
||||
entry and the old one a strike-through plus a pointer. The spec is updated in the same change.
|
||||
|
||||
| # | Date | Decision | Why | Where it lives |
|
||||
|---|---|---|---|---|
|
||||
| D-001 | YYYY-MM-DD | … | … | SPEC §… |
|
||||
@@ -0,0 +1,12 @@
|
||||
org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.configuration-cache=true
|
||||
android.useAndroidX=true
|
||||
android.nonTransitiveRClass=true
|
||||
kotlin.code.style=official
|
||||
# Pinned JDK: Homebrew openjdk@25 (current LTS). A versioned formula does not move under the build; the unversioned
|
||||
# `openjdk` on PATH does. Run ./gradlew from any shell and never set JAVA_HOME. Bytecode still targets Java 17.
|
||||
org.gradle.java.installations.paths=/opt/homebrew/opt/openjdk@25/libexec/openjdk.jdk/Contents/Home
|
||||
# Nothing is downloaded: if openjdk@25 is missing the build says so.
|
||||
org.gradle.java.installations.auto-download=false
|
||||
@@ -0,0 +1,2 @@
|
||||
# Gradle daemon JVM: JDK 25, the current LTS. Bytecode still targets Java 17.
|
||||
toolchainVersion=25
|
||||
@@ -0,0 +1,68 @@
|
||||
I'm starting a brand-new project and want your help producing a [SPEC.md](http://SPEC.md) that coding agents will build from.
|
||||
|
||||
## The idea
|
||||
|
||||
A Splitwise-style app for splitting shared expenses between me and my spouse. households, day-to-day and trip expenses. Working name: Kvitt.
|
||||
|
||||
## What I know so far
|
||||
|
||||
- Who it's for: Me and my partner
|
||||
|
||||
- Platforms: native Android app (Kotlin). Both have Samsung Galaxy S26 phones. One have normal and one have plus size.
|
||||
|
||||
- Hosting: On raspberry pi via docker at my home. I have a domain that points to my IP.
|
||||
|
||||
- Preferred stack: Kotlin for Android, server backend in rust, database which is lightweight enough for RPI but I also want nice analytics and graphs in the app for how we spend over time.
|
||||
|
||||
- Must-haves: Expenses, pre-defined uneven splits (i.e. me pay 60% and my partner pay 40%, configurable), balances, settle up, total spend each month (down to weeks) + ability to group by category of expense. Be able to invite my Spouse, set name on both of us + a profile picture. Also to have multiple "expense acocunts", like we want to have daily expenses, house expenses, travels etc. this was just examples so it needs to be dynamic, basically how Spendwise's groups work, but it will always be a group with me and my spouse. It needs to work offline, and then later sync once back online.
|
||||
|
||||
- Nice-to-haves: Receipt scanning, connect to our creditcard to figure out exact foreign exchange rate (We use Spendwise in Sweden, maybe they have an API?)
|
||||
|
||||
- Explicitly NOT doing: No bank integrations in v1,
|
||||
|
||||
## How I want you to work
|
||||
|
||||
1. Don't write the spec yet. First interview me, in rounds of 3–5
|
||||
|
||||
questions. Ask about anything that would change the architecture,
|
||||
|
||||
data model or scope. Challenge assumptions and point out things
|
||||
|
||||
I haven't thought of (e.g. debt simplification, rounding, currency,
|
||||
|
||||
deleted users, offline use, auth).
|
||||
|
||||
2. When you have enough, propose a v1 scope and a short list of
|
||||
|
||||
key decisions with your recommendation for each. Wait for my OK.
|
||||
|
||||
3. Then write [SPEC.md](http://SPEC.md) with these sections:
|
||||
|
||||
- Overview & goals
|
||||
|
||||
- Non-goals
|
||||
|
||||
- Users & core user stories
|
||||
|
||||
- Functional requirements (v1), with acceptance criteria
|
||||
|
||||
- Data model
|
||||
|
||||
- Architecture & stack (with reasoning)
|
||||
|
||||
- API outline
|
||||
|
||||
- Auth & security
|
||||
|
||||
- Edge cases & business rules (splits, rounding, settlement)
|
||||
|
||||
- Deployment & ops
|
||||
|
||||
- Milestones (small, independently shippable steps)
|
||||
|
||||
- Open questions
|
||||
|
||||
4. Make an explicit UX-sprint spec which a UX agent can do with some proposals for how the app could look like.
|
||||
5. Write it so multiple coding agents with no other context could implement it without guessing.
|
||||
|
||||
Start with your first round of questions.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Prompt: orchestrator session
|
||||
|
||||
Read, in this order: CLAUDE.md, docs/HANDOFF.md (§1–§4 are the current state, §6 the way of working), then SPEC.md's
|
||||
change log and docs/decisions.md from the last entry you know. Verify HANDOFF §1 against `main` (branches, worktrees,
|
||||
`./gradlew check`), tell me what you found, then act as my orchestrator: one Opus agent per task in its own worktree,
|
||||
builds only through `tools/gradle-serial.sh`, you land by rebase + gate + fast-forward in the main checkout, you never
|
||||
write code yourself, subagents Opus only. Reply short: status first, then exact HUMAN steps (section and button
|
||||
labels verbatim) or what you need from me. Numbers and logs you pull yourself; I read nothing off the phone.
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/bin/sh
|
||||
# Deletes Gradle and Kotlin build output in the main checkout and in every agent worktree (.claude/worktrees/*):
|
||||
# each project's build/ (a directory named build next to a build.gradle.kts or settings.gradle.kts), .gradle/configuration-cache
|
||||
# and .kotlin. Never looks into export/, libs/, models/, spike/ or "samsung sdk/", and never deeper than four levels.
|
||||
# Prints what it removes and the space freed. Waits for the Gradle lock of tools/gradle-serial.sh, so it never deletes
|
||||
# under a running build. The orchestrator runs it after removing a worktree or when the disk is low.
|
||||
# Usage from any checkout: tools/clean-builds.sh [--dry-run]
|
||||
dry=0
|
||||
case "$1" in
|
||||
--dry-run) dry=1 ;;
|
||||
"") ;;
|
||||
*) echo "usage: $0 [--dry-run]" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
common=$(git rev-parse --path-format=absolute --git-common-dir) || exit 1
|
||||
main=$(dirname "$common")
|
||||
|
||||
# Candidates under one checkout root; .claude is skipped here because the worktrees are walked as roots of their own.
|
||||
candidates() {
|
||||
find "$1" -maxdepth 4 \
|
||||
\( -path "$1/export" -o -path "$1/libs" -o -path "$1/models" -o -path "$1/spike" -o -path "$1/samsung sdk" \
|
||||
-o -path "$1/.claude" -o -name .git -o -name src -o -name node_modules \) -prune \
|
||||
-o -type d \( -name build -o -name .kotlin -o -path '*/.gradle/configuration-cache' \) -print -prune
|
||||
}
|
||||
|
||||
list=$(mktemp) || exit 1
|
||||
trap 'rm -f "$list"' EXIT
|
||||
for root in "$main" "$main"/.claude/worktrees/*; do
|
||||
[ -d "$root" ] || continue
|
||||
candidates "$root" | while IFS= read -r dir; do
|
||||
if [ "$(basename "$dir")" = build ]; then
|
||||
parent=$(dirname "$dir")
|
||||
[ -f "$parent/build.gradle.kts" ] || [ -f "$parent/settings.gradle.kts" ] || continue
|
||||
fi
|
||||
printf '%s\n' "$dir"
|
||||
done
|
||||
done > "$list"
|
||||
|
||||
if [ ! -s "$list" ]; then echo "Nothing to remove."; exit 0; fi
|
||||
|
||||
if [ "$dry" -eq 0 ]; then
|
||||
lock=/tmp/kvitt-gradle.lock
|
||||
while ! mkdir "$lock" 2>/dev/null; do
|
||||
pid=$(cat "$lock/pid" 2>/dev/null)
|
||||
if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then rm -rf "$lock"; continue; fi
|
||||
sleep 5
|
||||
done
|
||||
echo $$ > "$lock/pid"
|
||||
trap 'rm -rf "$lock"; rm -f "$list"' EXIT INT TERM HUP
|
||||
fi
|
||||
|
||||
total=0
|
||||
while IFS= read -r dir; do
|
||||
kb=$(du -sk "$dir" 2>/dev/null | cut -f1)
|
||||
kb=${kb:-0}
|
||||
total=$((total + kb))
|
||||
printf '%8d MB %s\n' $((kb / 1024)) "${dir#"$main"/}"
|
||||
[ "$dry" -eq 1 ] || rm -rf "$dir"
|
||||
done < "$list"
|
||||
|
||||
if [ "$dry" -eq 1 ]; then verb="Would free"; else verb="Freed"; fi
|
||||
echo "$verb $((total / 1024)) MB in $(wc -l < "$list" | tr -d ' ') directories."
|
||||
Executable
+147
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Version drift report for gradle/libs.versions.toml (Plan 09, D-056).
|
||||
|
||||
Reads every [versions] entry, finds an artifact that uses it in [libraries] or [plugins], and asks Google Maven and
|
||||
Maven Central for that artifact's maven-metadata.xml. Prints one table: current, latest stable, newest (only where a
|
||||
pre-release is newer than the latest stable), and how far the current version is behind.
|
||||
The Gradle wrapper version is compared with services.gradle.org. Nothing here runs from the build; run it by hand:
|
||||
|
||||
python3 tools/dependency-report/latest.py
|
||||
|
||||
Rule (CLAUDE.md › Build and verify): run it at the start of every plan. A component more than one stable minor
|
||||
behind gets a line in that plan's report; a toolchain component (Gradle, AGP, Kotlin, JDK LTS) more than one line
|
||||
behind triggers a Plan 09-shaped upgrade before the plan starts.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import urllib.request
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
REPOS = ["https://dl.google.com/android/maven2", "https://repo1.maven.org/maven2"]
|
||||
# Catalog entries that are SDK levels, not artifacts.
|
||||
NOT_ARTIFACTS = {"minSdk", "compileSdk", "targetSdk"}
|
||||
# The toolchain is looked up by its Gradle plugin, not by the first library that shares its version.
|
||||
PREFERRED = {
|
||||
"agp": ("com.android.tools.build", "gradle"),
|
||||
"kotlin": ("org.jetbrains.kotlin", "kotlin-gradle-plugin"),
|
||||
"ksp": ("com.google.devtools.ksp", "symbol-processing-gradle-plugin"),
|
||||
}
|
||||
STABLE = re.compile(r"^\d+(\.\d+)*$")
|
||||
|
||||
|
||||
def key(version):
|
||||
"""Numeric parts first; a pre-release (anything after the first '-') sorts before its release."""
|
||||
base, _, suffix = version.partition("-")
|
||||
numbers = [int(p) if p.isdigit() else -1 for p in base.split(".")]
|
||||
return numbers, (0, suffix) if suffix else (1, "")
|
||||
|
||||
|
||||
def fetch(url):
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=20) as response:
|
||||
return response.read()
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
def versions_of(group, name):
|
||||
"""Versions from every repository together: some artifacts (early KSP) also sit, stale, on Google Maven."""
|
||||
found = set()
|
||||
for repo in REPOS:
|
||||
body = fetch(f"{repo}/{group.replace('.', '/')}/{name}/maven-metadata.xml")
|
||||
if body:
|
||||
found.update(v.text for v in ET.fromstring(body).iter("version") if v.text)
|
||||
return sorted(found)
|
||||
|
||||
|
||||
def plugin_marker(plugin_id):
|
||||
return plugin_id, f"{plugin_id}.gradle.plugin"
|
||||
|
||||
|
||||
def read_catalog(text):
|
||||
"""The subset of TOML the catalog uses (works on Python < 3.11, which has no tomllib): returns the [versions]
|
||||
entries in file order and, per version name, the first library or plugin that refers to it."""
|
||||
versions, artifacts, section = {}, {}, None
|
||||
for raw in text.splitlines():
|
||||
line = raw.split("#", 1)[0].strip()
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("["):
|
||||
section = line.strip("[]")
|
||||
continue
|
||||
if section == "versions":
|
||||
match = re.match(r'([\w-]+)\s*=\s*"([^"]+)"', line)
|
||||
if match:
|
||||
versions[match.group(1)] = match.group(2)
|
||||
continue
|
||||
ref = re.search(r'version\.ref\s*=\s*"([^"]+)"', line)
|
||||
if not ref or ref.group(1) in artifacts:
|
||||
continue
|
||||
module = re.search(r'module\s*=\s*"([^"]+)"', line)
|
||||
plugin = re.search(r'\bid\s*=\s*"([^"]+)"', line)
|
||||
if section == "libraries" and module:
|
||||
artifacts[ref.group(1)] = tuple(module.group(1).split(":"))
|
||||
elif section == "plugins" and plugin:
|
||||
artifacts[ref.group(1)] = plugin_marker(plugin.group(1))
|
||||
return versions, artifacts
|
||||
|
||||
|
||||
def behind(current, stable):
|
||||
if not stable or not STABLE.match(current):
|
||||
return ""
|
||||
c, s = [int(x) for x in current.split(".")], [int(x) for x in stable.split(".")]
|
||||
c += [0] * (3 - len(c))
|
||||
s += [0] * (3 - len(s))
|
||||
if c[0] < s[0]:
|
||||
return f"{s[0] - c[0]} major"
|
||||
if c[1] < s[1]:
|
||||
return f"{s[1] - c[1]} minor"
|
||||
if c[2] < s[2]:
|
||||
return "patch"
|
||||
return ""
|
||||
|
||||
|
||||
def row(name, coordinate, current, available):
|
||||
stable = sorted((v for v in available if STABLE.match(v)), key=key)
|
||||
everything = sorted(available, key=key)
|
||||
latest = stable[-1] if stable else "?"
|
||||
newest = everything[-1] if everything else "?"
|
||||
return [name, coordinate, current, latest, newest if newest not in (latest, current) else "", behind(current, latest if stable else "")]
|
||||
|
||||
|
||||
def gradle_row():
|
||||
props = (ROOT / "gradle/wrapper/gradle-wrapper.properties").read_text()
|
||||
match = re.search(r"gradle-([\d.]+)-(bin|all)\.zip", props)
|
||||
current = match.group(1) if match else "?"
|
||||
body = fetch("https://services.gradle.org/versions/all")
|
||||
available = []
|
||||
if body:
|
||||
available = [v["version"] for v in json.loads(body) if not v.get("snapshot") and not v.get("nightly")]
|
||||
return row("gradle (wrapper)", "services.gradle.org", current, available)
|
||||
|
||||
|
||||
def main():
|
||||
versions, artifacts = read_catalog((ROOT / "gradle/libs.versions.toml").read_text())
|
||||
rows = [gradle_row()]
|
||||
for name, current in versions.items():
|
||||
if name in NOT_ARTIFACTS:
|
||||
continue
|
||||
coordinate = PREFERRED.get(name) or artifacts.get(name)
|
||||
if not coordinate:
|
||||
rows.append([name, "(no artifact)", str(current), "", "", ""])
|
||||
continue
|
||||
rows.append(row(name, ":".join(coordinate), str(current), versions_of(*coordinate)))
|
||||
header = ["version", "artifact", "current", "latest stable", "newer pre-release", "behind"]
|
||||
widths = [max(len(str(r[i])) for r in rows + [header]) for i in range(len(header))]
|
||||
print(" | ".join(h.ljust(w) for h, w in zip(header, widths)))
|
||||
print("-+-".join("-" * w for w in widths))
|
||||
for r in rows:
|
||||
print(" | ".join(str(c).ljust(w) for c, w in zip(r, widths)))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
# Kvitt pre-commit hook: the fast subset of `./gradlew check`. Installed by tools/install-git-hooks.sh.
|
||||
set -e
|
||||
top=$(git rev-parse --show-toplevel)
|
||||
|
||||
# Secrets and personal data never reach git. Adjust the list to this project (see CLAUDE.md › Never commit).
|
||||
bad=$(git -C "$top" ls-files --cached -- local.properties '*.jks' '*.keystore' '*.p12' '*.aar' 'models/' 'export/' | head -5)
|
||||
if [ -n "$bad" ]; then
|
||||
echo "pre-commit: a secret, keystore, vendor binary or personal-data file is staged or tracked:" >&2
|
||||
echo "$bad" >&2
|
||||
echo "Unstage with: git rm --cached <file>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[ -x "$top/gradlew" ] || exit 0
|
||||
|
||||
# The build runs on the pinned JDK (gradle.properties); the shell's default java may be newer or a macOS stub.
|
||||
case "$("${JAVA_HOME:+$JAVA_HOME/bin/}java" -version 2>&1 | head -1)" in
|
||||
*\"25*) ;;
|
||||
*) [ -d /opt/homebrew/opt/openjdk@25 ] && export JAVA_HOME=/opt/homebrew/opt/openjdk@25/libexec/openjdk.jdk/Contents/Home ;;
|
||||
esac
|
||||
|
||||
cd "$top"
|
||||
# Replace with this project's fast checks (architecture rules, the pure-module tests). Keep it under a minute.
|
||||
./gradlew --quiet :core:model:test || {
|
||||
echo "pre-commit: fast tests failed (see above). Run ./gradlew check for the full picture." >&2
|
||||
exit 1
|
||||
}
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
# Runs ./gradlew with the given arguments, one Gradle build at a time across every worktree and agent on this machine
|
||||
# (CLAUDE.md › Build and verify: parallel builds exhaust memory). Waits for a running build to finish, then runs.
|
||||
# Usage from any worktree root: tools/gradle-serial.sh check
|
||||
lock=/tmp/kvitt-gradle.lock
|
||||
top=$(git rev-parse --show-toplevel) || exit 1
|
||||
while ! mkdir "$lock" 2>/dev/null; do
|
||||
pid=$(cat "$lock/pid" 2>/dev/null)
|
||||
if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then rm -rf "$lock"; continue; fi
|
||||
sleep 5
|
||||
done
|
||||
echo $$ > "$lock/pid"
|
||||
trap 'rm -rf "$lock"' EXIT INT TERM HUP
|
||||
cd "$top" && ./gradlew "$@"
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Installs the Kvitt pre-commit hook into this repository's hooks folder (shared by all worktrees).
|
||||
# The hook guards itself: on branches without the app build it only runs the export/ check.
|
||||
set -euo pipefail
|
||||
here="$(cd "$(dirname "$0")" && pwd)"
|
||||
hooks="$(git -C "$here" rev-parse --path-format=absolute --git-common-dir)/hooks"
|
||||
target="$hooks/pre-commit"
|
||||
if [[ -e "$target" ]] && ! cmp -s "$here/git-hooks/pre-commit" "$target"; then
|
||||
echo "A different pre-commit hook already exists at $target; not overwriting. Merge them by hand." >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$hooks"
|
||||
cp "$here/git-hooks/pre-commit" "$target"
|
||||
chmod +x "$target"
|
||||
echo "Installed $target"
|
||||
Reference in New Issue
Block a user