diff --git a/.claude/agents/pr-reviewer.md b/.claude/agents/pr-reviewer.md new file mode 100644 index 0000000..033f740 --- /dev/null +++ b/.claude/agents/pr-reviewer.md @@ -0,0 +1,55 @@ +--- +name: pr-reviewer +description: Use for deep PR reviews. Reads the diff against Oikos Hard Constraints and returns a structured verdict bucketed into Blocking / Should fix / Nice to have with file:line references. Isolated context keeps the full diff out of the main thread. +tools: Read, Grep, Glob, Bash(gh pr *), Bash(gh api *), Bash(git diff *), Bash(git log *) +model: opus +memory: project +color: orange +--- + +You are reviewing a single PR for Oikos, a self-hosted family planner PWA. The parent thread has delegated the deep read to you so its context stays free. + +## Inputs + +Expect the parent to pass the PR number. Start with `gh pr view --repo ulsklyc/oikos --json title,body,headRefName,baseRefName,files,author,state` and `gh pr diff --repo ulsklyc/oikos`. + +## How to read + +Check every changed file against the Hard Constraints in `CLAUDE.md`: + +- Frontend: no frameworks, no bundlers, no CSS libraries. Lucide is the only exception and must stay self-hosted. +- ES modules only (`import`/`export`). No `require`. +- No `eval`. No `innerHTML` writes of any kind — including static SVG strings. The PostToolUse hook enforces this but reviewers catch what escapes. +- All UI text goes through `t('key')` from `public/i18n.js`. `de` is the reference locale and must contain every new key. +- Dates via `formatDate()` / `formatTime()`. No manual formatting. +- `server/db.js` migrations array is append-only. Flag any edit to an existing entry as Blocking. +- Design values come from `public/styles/tokens.css`. Flag raw hex, px, rem values in CSS. +- Every route handler wrapped in try/catch. Response shape `{ data }` on success, `{ error, code }` on failure. +- Tests: `test-.js` at project root, registered as `test:` in `package.json`, `--experimental-sqlite` flag in the script. +- `CHANGELOG.md` has a new bullet under `## [Unreleased]`. + +## Output format + +Return a single markdown block grouped as: + +``` +## Blocking +- `path/to/file.js:42` — + +## Should fix +- `path/to/file.js:120` — + +## Nice to have +- ... + +## Verdict + +``` + +Be specific. Quote the offending line. Cite which constraint is violated. Never list things that are fine. + +## Hard rules + +- English only. +- Never post the review yourself. Return the markdown and let the parent invoke `gh pr review`. +- If the diff is huge (>1000 lines) and clearly outside scope, return `close` with a short explanation — don't try to nitpick. diff --git a/.claude/agents/repo-auditor.md b/.claude/agents/repo-auditor.md new file mode 100644 index 0000000..cded9da --- /dev/null +++ b/.claude/agents/repo-auditor.md @@ -0,0 +1,55 @@ +--- +name: repo-auditor +description: Monthly health sweep for the Oikos repo. Surfaces stale issues, dormant branches, untracked TODOs, outdated deps, dead test files, and un-released commits. Runs in a worktree so the main checkout stays untouched. +tools: Read, Grep, Glob, Bash(gh *), Bash(git *), Bash(npm outdated *) +model: sonnet +isolation: worktree +memory: project +color: purple +--- + +You are auditing the Oikos repo. Report only — never push, never close, never file PRs. Returning a concise markdown report is the entire job. + +## Checks + +Run all six and report each even if clean. + +1. **Stale issues** — `gh issue list --repo ulsklyc/oikos --state open --json number,title,updatedAt,labels`. Flag any open issue with `updatedAt` older than 90 days or labelled `needs-info` for >14 days. +2. **Dormant branches** — `git branch -r --format '%(refname:short) %(committerdate:iso)'`. Flag remote branches with no commits in 30+ days that aren't `main` or a protected branch. +3. **Untracked TODOs** — `grep -rn "TODO\|FIXME\|XXX\|HACK" --include='*.js' --include='*.css' --include='*.md' --exclude-dir=node_modules`. Cross-reference with open issues. Flag TODOs that don't link to an issue number. +4. **Outdated deps** — `npm outdated --json` inside `oikos/`. Flag anything with a major upgrade available and anything with a known CVE (check `gh api /repos/ulsklyc/oikos/dependabot/alerts` if dependabot is on). +5. **Dead test files** — list all `test-*.js` at `oikos/` root, cross-check against `package.json` scripts. Flag any `test-*.js` not wired into a `test:*` script. Flag any `test:*` script pointing at a missing file. +6. **Un-released commits** — `git log v..main --oneline`. If there are commits on `main` beyond the latest tag AND `## [Unreleased]` in `CHANGELOG.md` has bullets, recommend running `/release-prep`. + +## Output format + +``` +# Repo audit — + +## Stale issues +- # — <days> days idle + +## Dormant branches +- <branch> — last commit <ISO date> + +## Untracked TODOs +- `<file>:<line>` — <excerpt> + +## Outdated dependencies +- <pkg> <current> → <latest> (<type>) + +## Dead test files +- <filename> — <reason> + +## Release status +<one sentence> + +## Suggested actions +<3-5 bullets, ordered by impact> +``` + +## Hard rules + +- Read-only. No `git push`, no `gh issue close`, no file edits. +- Stick to facts visible in the repo. Don't speculate about user intent. +- If a check returns nothing, write `none` — don't omit the section. diff --git a/.claude/hooks/block-innerhtml.sh b/.claude/hooks/block-innerhtml.sh new file mode 100755 index 0000000..f9b8e2a --- /dev/null +++ b/.claude/hooks/block-innerhtml.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +INPUT=$(cat) +FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty') + +case "$FILE_PATH" in + *.js|*.mjs|*.html) ;; + *) exit 0 ;; +esac + +[ -f "$FILE_PATH" ] || exit 0 + +MATCHES=$(grep -nE '\.innerHTML[[:space:]]*[+]?=[^=]' "$FILE_PATH" || true) + +if [ -n "$MATCHES" ]; then + jq -n --arg matches "$MATCHES" --arg path "$FILE_PATH" '{ + decision: "block", + reason: ("innerHTML write detected in " + $path + ":\n" + $matches + "\n\nUse textContent, createElement, createElementNS, replaceChildren, or appendChild. See CLAUDE.md Hard Constraints."), + hookSpecificOutput: { + hookEventName: "PostToolUse", + additionalContext: "Revert this change and use DOM APIs. The innerHTML ban has no exceptions — not even for static SVG sprite strings." + } + }' + exit 0 +fi + +exit 0 diff --git a/.claude/rules/db-migrations.md b/.claude/rules/db-migrations.md new file mode 100644 index 0000000..e525f74 --- /dev/null +++ b/.claude/rules/db-migrations.md @@ -0,0 +1,14 @@ +--- +name: db-migrations +description: Append-only migration rules for server/db.js +paths: + - oikos/server/db.js +--- + +- The `migrations` array in this file is **append-only**. Never modify, reorder, split, merge, or delete an existing entry. Even fixing a typo in an existing migration SQL string is forbidden. +- New work goes into a NEW entry appended to the end of the array. Give it the next monotonic `version` number. +- Each migration's SQL must be idempotent where possible (`CREATE TABLE IF NOT EXISTS`, `ALTER TABLE ... ADD COLUMN` guarded by a prior check). Runtime catches errors and logs them but the migration should not need the catch to succeed. +- The `schema_migrations` table tracks which versions have run on a given DB. Changing or renumbering an existing entry silently skips the change on every upgraded install — which is why the append-only rule exists. +- If you need to fix a bad migration, append a new migration that performs the fix. Never rewrite history. +- Production DBs may have data created by every prior migration. Test new migrations against a copy of a populated DB when the change touches existing tables. +- No data loss migrations without explicit user sign-off in the PR description. Adding a column with `NOT NULL` and no default on a populated table counts as data loss. diff --git a/.claude/rules/public-pages.md b/.claude/rules/public-pages.md new file mode 100644 index 0000000..72166f8 --- /dev/null +++ b/.claude/rules/public-pages.md @@ -0,0 +1,18 @@ +--- +name: public-pages +description: Rules for frontend pages and web components +paths: + - oikos/public/pages/**/*.js + - oikos/public/components/**/*.js +--- + +- **Pages** (`public/pages/*.js`) export an async `render(container, params)` function. No side effects on import — only the default `render` export may mutate state. Pages are invoked by `public/router.js`. +- **Components** (`public/components/*.js`) — one custom element per file, class name ends in `Element`, tag name uses the `oikos-` prefix (`<oikos-task-list>`). Register once per file with `customElements.define`. +- **UI text** — every string the user reads goes through `t('key')` from `public/i18n.js`. Never hardcode German, English, or any other language. Add the key to every locale file under `public/locales/`. `de` is the reference locale and must stay complete. +- **Dates and times** — `formatDate(value, opts)` and `formatTime(value, opts)` from `public/i18n.js`. Never call `toLocaleString`, `toISOString`, `Intl.DateTimeFormat` directly in pages or components. +- **No `innerHTML`** — not for user data, not for static SVG strings, not for templates. Use `document.createElement`, `document.createElementNS` (for SVG), `replaceChildren`, `appendChild`, `textContent`. The PostToolUse hook blocks violations on save. +- **API calls** — go through `apiFetch()` from `public/api.js`. It handles CSRF, session expiry, and error envelope. Never call `fetch()` directly from a page. +- **Navigation** — use `router.navigate(path)` from `public/router.js`. Never set `location.href` or `location.pathname` directly. +- **Styling** — reference tokens from `public/styles/tokens.css`. No raw hex, rgb(), rem, or px in component CSS — use `var(--token-name)`. +- **Icons** — use the self-hosted Lucide helper if one exists in the repo, otherwise `createElementNS('http://www.w3.org/2000/svg', ...)`. Never inline an SVG via `innerHTML`. +- **Lifecycle** — components must clean up listeners in `disconnectedCallback`. Debounce or throttle heavy handlers via `public/utils/ux.js`. diff --git a/.claude/rules/server-routes.md b/.claude/rules/server-routes.md new file mode 100644 index 0000000..fb984da --- /dev/null +++ b/.claude/rules/server-routes.md @@ -0,0 +1,15 @@ +--- +name: server-routes +description: Rules for Express route handlers under server/routes/ +paths: + - oikos/server/routes/**/*.js +--- + +- Every route handler wraps its body in `try/catch`. The catch path logs via the existing logger and returns `{ error: string, code: number }` with an appropriate HTTP status. No unhandled promise rejections. +- Success responses return `{ data: ... }`. Never return a raw array or primitive. +- Validate input at the boundary: request body, params, query. Reject with 400 on malformed input before touching the DB. +- Session + CSRF are enforced by middleware in `server/middleware/`. Don't re-implement auth inside a handler. Don't skip CSRF on mutating routes. +- Dates: accept and emit ISO 8601 strings. Store as TEXT in SQLite. Convert to `Date` only at the edges. +- `better-sqlite3` is synchronous. Never `await` a `db.prepare()` / `.run()` / `.get()` / `.all()` call. If you find an `await` in front of a db call, it's a bug. +- No `innerHTML` anywhere (server-side string building into HTML is fine as long as it doesn't end up in a frontend `innerHTML`; prefer JSON). +- Route files export a factory `(db) => router` pattern consistent with existing files in this directory. Read a neighbour file before adding a new one. diff --git a/.claude/rules/tests.md b/.claude/rules/tests.md new file mode 100644 index 0000000..67b437c --- /dev/null +++ b/.claude/rules/tests.md @@ -0,0 +1,14 @@ +--- +name: tests +description: Rules for integration test files at project root +paths: + - oikos/test-*.js +--- + +- File names match the pattern `test-<module>.js` and live at the project root alongside `package.json`. +- Every test file has a matching npm script `test:<module>` in `package.json`. The `test` aggregate script runs them all. When you add a new `test-*.js`, add it to both places. +- Runner is `node --test` (built-in, Node ≥22). Scripts that hit the DB pass `--experimental-sqlite`. +- Database in tests is an in-memory `better-sqlite3` instance built via the same `buildSchema` path used by production. Never mock `better-sqlite3`. Never stub out migrations. If a test needs seed data, insert it through normal route handlers or the same queries prod uses. +- Assertions via `node:assert/strict`. Imports use `import` syntax. No `require`. +- Tests must be deterministic. No network calls, no real filesystem writes outside `os.tmpdir()`, no `Date.now()` without faking. +- A failing test is a real failure. Don't wrap in `t.skip` or comment out to make CI green. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..1a86153 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,17 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Write|Edit", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-innerhtml.sh", + "timeout": 10, + "statusMessage": "Checking for innerHTML writes..." + } + ] + } + ] + } +} diff --git a/.claude/skills/fix-issue/SKILL.md b/.claude/skills/fix-issue/SKILL.md new file mode 100644 index 0000000..1748224 --- /dev/null +++ b/.claude/skills/fix-issue/SKILL.md @@ -0,0 +1,36 @@ +--- +name: fix-issue +description: Take a GitHub issue from triage through a PR on a fix/<id> branch +disable-model-invocation: true +user-invocable: true +argument-hint: "<issue-number>" +allowed-tools: + - Bash(gh issue *) + - Bash(gh pr *) + - Bash(git checkout *) + - Bash(git add *) + - Bash(git commit *) + - Bash(git push *) + - Bash(npm test *) + - Bash(npm run test:*) + - Read + - Edit + - Write + - Grep + - Glob +--- + +Run from inside `oikos/`. `$1` is the issue number. + +1. **Load context** — `gh issue view $1 --repo ulsklyc/oikos --comments`. Read linked PRs, commits, related issues. Stop and report if the issue is already closed or duplicated. +2. **Triage before coding** — classify: bug, enhancement, question, invalid. If reproduction steps are missing or scope is unclear, post a question via `gh issue comment $1 --body "..."` and stop. Do not guess intent. +3. **Branch + implement** — `git checkout -b fix/$1`. Make the minimal change that solves the reported problem. Respect the Hard Constraints from CLAUDE.md. Add or extend a `test-<module>.js` suite that would have caught the bug. Run `npm test` — all suites must pass before moving on. +4. **Ship** — `git add` only the files actually changed by this fix, commit with a Conventional Commit subject (`fix: <short summary> (#$1)`), push `git push -u origin fix/$1`, then `gh pr create --fill --base main` with a body that closes the issue (`Closes #$1`) and summarises root cause + fix. + +## Guardrails + +- Never work on `main` directly. If you're accidentally on `main`, stop and switch. +- Never bypass the PostToolUse innerHTML hook. If it fires, fix the DOM code — don't disable the hook. +- Never `git add -A` or `git add .`. Stage files by name. +- If the fix needs a DB change, it goes into a NEW entry at the end of the `migrations` array in `server/db.js`. Never edit existing entries. +- Do not run the `release-prep` skill here — releases happen after the PR is merged, on `main`. diff --git a/.claude/skills/issue-triage/SKILL.md b/.claude/skills/issue-triage/SKILL.md new file mode 100644 index 0000000..393d3b8 --- /dev/null +++ b/.claude/skills/issue-triage/SKILL.md @@ -0,0 +1,31 @@ +--- +name: issue-triage +description: Classify one or all open issues, apply labels, request missing info +disable-model-invocation: true +user-invocable: true +argument-hint: "[<issue-number> | all]" +allowed-tools: + - Bash(gh issue *) + - Bash(gh label *) + - Read + - Grep +--- + +Run from inside `oikos/`. `$1` is a specific issue number or the literal `all` (default: `all` → every open issue without labels). + +1. **Load** — for a single issue: `gh issue view $1 --repo ulsklyc/oikos --comments`. For `all`: `gh issue list --repo ulsklyc/oikos --state open --label '' --json number,title,body,author,createdAt`. +2. **Classify** — for each issue, pick ONE primary class and apply labels via `gh issue edit <n> --repo ulsklyc/oikos --add-label "<labels>"`: + - `bug` — reproduction + expected vs. actual behaviour present and plausible against current code + - `enhancement` — new feature or UX improvement, no existing regression + - `question` — user needs help using Oikos, not a code change + - `invalid` — spam, duplicate, or out of scope (self-hosted family planner) + Add area labels where obvious: `calendar`, `tasks`, `shopping`, `meals`, `budget`, `notes`, `contacts`, `reminders`, `i18n`, `pwa`, `security`, `docs`. +3. **Request missing info** — if reproduction steps, expected behaviour, Oikos version, or browser are missing on a `bug`, post a single comment asking for exactly what's missing. Apply label `needs-info`. +4. **Close spam/duplicates** — `gh issue close <n> --repo ulsklyc/oikos --reason "not planned" --comment "<english explanation>"`. Always leave a reason. + +## Guardrails + +- Never assign issues to other humans. +- Never post more than one triage comment per issue per run. +- All comments in English. +- If unsure between two classes, default to `question` and ask for clarification — don't guess. diff --git a/.claude/skills/pr-review/SKILL.md b/.claude/skills/pr-review/SKILL.md new file mode 100644 index 0000000..aad8fdf --- /dev/null +++ b/.claude/skills/pr-review/SKILL.md @@ -0,0 +1,41 @@ +--- +name: pr-review +description: Review a PR against the Oikos Hard Constraints and decide close/request-changes/merge +disable-model-invocation: true +user-invocable: true +argument-hint: "<pr-number>" +allowed-tools: + - Bash(gh pr *) + - Bash(gh api *) + - Bash(git *) + - Bash(npm test *) + - Bash(npm run test:*) + - Read + - Grep + - Glob +--- + +Run from inside `oikos/`. `$1` is the PR number. + +1. **Fetch** — `gh pr view $1 --repo ulsklyc/oikos`, `gh pr diff $1 --repo ulsklyc/oikos`, `gh pr checks $1 --repo ulsklyc/oikos`. Note failing checks. Delegate the deep read to the `pr-reviewer` subagent (`Agent({ subagent_type: "pr-reviewer", ... })`) so main-thread context stays free. +2. **Constraint check** — walk the diff against CLAUDE.md Hard Constraints: + - No frontend frameworks / bundlers / CSS libraries added + - No `require`, only `import`/`export` + - No `eval`, no `innerHTML` + - All UI text via `t('key')`; `de` locale updated + - Migrations append-only (`server/db.js`) + - Design values from `public/styles/tokens.css` + - Route handlers wrapped in try/catch + - API shape `{ data }` / `{ error, code }` + Also check: PR has tests touching the relevant `test-<module>.js`, CHANGELOG entry under `## [Unreleased]`, commit subjects are Conventional Commits. +3. **Decide** + - **Blocking issue found** → `gh pr review $1 --request-changes --body "<english, grouped by file:line>"` and stop. + - **Not a fit** → explain politely and `gh pr close $1 --comment "..."`. + - **Clean** → `gh pr review $1 --approve --body "LGTM"` then `gh pr merge $1 --squash --delete-branch`. After merge, fetch `main` locally and consider running `/release-prep`. + +## Guardrails + +- Comments and review bodies: always English. +- Never merge with failing required checks. Never force-merge. +- Never close a PR without a reason comment — silent closes burn contributor trust. +- Never push directly to the PR branch. If a small fix is warranted, ask the contributor. diff --git a/.claude/skills/release-prep/SKILL.md b/.claude/skills/release-prep/SKILL.md new file mode 100644 index 0000000..900c973 --- /dev/null +++ b/.claude/skills/release-prep/SKILL.md @@ -0,0 +1,37 @@ +--- +name: release-prep +description: Bump patch version, update CHANGELOG, commit, tag, push and create GitHub release +disable-model-invocation: true +user-invocable: true +argument-hint: "[patch|minor|major]" +allowed-tools: + - Read + - Edit + - Bash(npm version *) + - Bash(git add *) + - Bash(git commit *) + - Bash(git tag *) + - Bash(git push *) + - Bash(gh release create *) + - Bash(git status *) + - Bash(git log *) + - Bash(git diff *) +--- + +Run from inside `oikos/`. Argument selects the semver bump (default: patch). + +1. **Pre-flight** — run `git status` and `git diff --staged`. Abort if the tree is dirty with unrelated files. Summarise the pending changes. +2. **CHANGELOG** — open `oikos/CHANGELOG.md`. Insert a new `## [X.Y.Z] - YYYY-MM-DD` block immediately below `## [Unreleased]`. Use today's date from the `currentDate` context. Only use Keep-a-Changelog sections: `### Added`, `### Changed`, `### Fixed`, `### Removed`, `### Security`. One bullet per user-facing change in English. Never invent entries that aren't in the diff. +3. **Version bump** — run `npm version ${1:-patch} --no-git-tag-version`. Read the new version from `oikos/package.json`. +4. **Stage** — `git add oikos/CHANGELOG.md oikos/package.json oikos/package-lock.json` plus any other files from the task. Never use `git add -A` or `git add .`. +5. **Commit** — `git commit -m "chore: release vX.Y.Z"`. Do not pass `--no-verify`. If a hook fails, fix the cause and create a new commit. +6. **Tag** — `git tag vX.Y.Z`. +7. **Push** — `git push && git push --tags`. +8. **GitHub Release** — `gh release create vX.Y.Z --repo ulsklyc/oikos --title "vX.Y.Z" --notes "<CHANGELOG block body>"`. Paste the new CHANGELOG section verbatim as notes. + +## Guardrails + +- Never `--force`, never `--no-verify`, never `--no-gpg-sign`. +- The `GH_TOKEN` must come from the shell environment, never from a hard-coded literal in this file or in commit messages. +- If `gh release create` fails with a 401/403, stop and report — do not paste a token inline. +- If `git status` shows uncommitted work unrelated to the release, stop and ask the user. diff --git a/.gitignore b/.gitignore index 8afcbd8..7e74964 100644 --- a/.gitignore +++ b/.gitignore @@ -31,8 +31,9 @@ data/ *.swp *.swo -# Claude Code (contains local permissions and possibly tokens) -.claude/ +# Claude Code — share skills/agents/rules/hooks/settings; keep local permissions and worktrees out +.claude/settings.local.json +.claude/worktrees/ # Git worktrees .worktrees/ diff --git a/CHANGELOG.md b/CHANGELOG.md index bbd9291..3d8e6ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.20.42] - 2026-04-21 + +### Added +- `.claude/` tooling committed with the repo: skills (`release-prep`, `fix-issue`, `pr-review`, `issue-triage`), subagents (`pr-reviewer`, `repo-auditor`), path-scoped rules (`server-routes`, `public-pages`, `tests`, `db-migrations`), and a PostToolUse hook (`block-innerhtml.sh`) that enforces the innerHTML ban on save. Contributors using Claude Code now get the same guardrails and workflows automatically. + +### Changed +- `.gitignore`: no longer excludes the entire `.claude/` directory — only `.claude/settings.local.json` and `.claude/worktrees/` stay out, so shared tooling is versioned while local permissions remain private. + ## [0.20.41] - 2026-04-21 ### Fixed diff --git a/package-lock.json b/package-lock.json index 7753f01..e4d6759 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "oikos", - "version": "0.20.41", + "version": "0.20.42", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "oikos", - "version": "0.20.41", + "version": "0.20.42", "license": "MIT", "dependencies": { "bcrypt": "^6.0.0", diff --git a/package.json b/package.json index e9b1b4e..9cfba09 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oikos", - "version": "0.20.41", + "version": "0.20.42", "description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.", "main": "server/index.js", "type": "module",