chore: release v0.20.42

This commit is contained in:
Ulas Kalayci
2026-04-21 10:38:45 +02:00
parent d1ec7367a0
commit 8f55855364
16 changed files with 375 additions and 5 deletions
+55
View File
@@ -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 <n> --repo ulsklyc/oikos --json title,body,headRefName,baseRefName,files,author,state` and `gh pr diff <n> --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-<module>.js` at project root, registered as `test:<module>` 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` — <concrete reason tied to a constraint>
## Should fix
- `path/to/file.js:120` — <reason>
## Nice to have
- ...
## Verdict
<one sentence: request-changes | approve | close>
```
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.
+55
View File
@@ -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<latest-tag>..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 — <ISO date>
## Stale issues
- #<n> <title> — <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.
+28
View File
@@ -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
+14
View File
@@ -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.
+18
View File
@@ -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`.
+15
View File
@@ -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.
+14
View File
@@ -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.
+17
View File
@@ -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..."
}
]
}
]
}
}
+36
View File
@@ -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`.
+31
View File
@@ -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.
+41
View File
@@ -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.
+37
View File
@@ -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.
+3 -2
View File
@@ -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/
+8
View File
@@ -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
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -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",