From cbf70665f8e87c9a3ffd77066576918a98357b04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=BChl?= Date: Thu, 26 Feb 2026 17:10:07 +0100 Subject: [PATCH] chore: add Forgejo PR workflow --- .claude/hooks/log-request.sh | 54 ++++++++++++++ .claude/hooks/session-start.sh | 39 +++++++++++ .claude/request-log.jsonl | 0 .claude/scripts/create-pr.sh | 100 ++++++++++++++++++++++++++ .claude/scripts/show-history.sh | 84 ++++++++++++++++++++++ .claude/scripts/update-entry.sh | 66 ++++++++++++++++++ .claude/settings.json | 29 ++++++++ .gitignore | 6 ++ CLAUDE.md | 120 ++++++++++++++++++++++++++++++++ 9 files changed, 498 insertions(+) create mode 100755 .claude/hooks/log-request.sh create mode 100755 .claude/hooks/session-start.sh create mode 100644 .claude/request-log.jsonl create mode 100755 .claude/scripts/create-pr.sh create mode 100755 .claude/scripts/show-history.sh create mode 100755 .claude/scripts/update-entry.sh create mode 100644 .claude/settings.json diff --git a/.claude/hooks/log-request.sh b/.claude/hooks/log-request.sh new file mode 100755 index 0000000..477d49e --- /dev/null +++ b/.claude/hooks/log-request.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Claude Code — UserPromptSubmit hook +# +# Logs each new user prompt to .claude/request-log.jsonl (git-tracked). +# Runs async so it never blocks Claude's response. +# +# Input (stdin): JSON with fields: session_id, cwd, prompt, is_continuation + +set -euo pipefail + +INPUT=$(cat) + +PROMPT=$(echo "$INPUT" | jq -r '.prompt // ""') +SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""') +CWD=$(echo "$INPUT" | jq -r '.cwd // "$PWD"') +IS_CONTINUATION=$(echo "$INPUT" | jq -r '.is_continuation // false') + +LOG_FILE="${CWD}/.claude/request-log.jsonl" + +# Skip: continuation turns, trivially short prompts, no log file dir +if [ "$IS_CONTINUATION" = "true" ]; then + exit 0 +fi + +if [ "${#PROMPT}" -lt 5 ]; then + exit 0 +fi + +if [ ! -d "${CWD}/.claude" ]; then + exit 0 +fi + +TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") +REQ_ID="req_$(date +%Y%m%d_%H%M%S)_$(LC_ALL=C tr -dc 'a-z0-9' < /dev/urandom | head -c 5)" +BRANCH=$(git -C "$CWD" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") + +jq -n \ + --arg id "$REQ_ID" \ + --arg ts "$TIMESTAMP" \ + --arg prompt "$PROMPT" \ + --arg session "$SESSION_ID" \ + --arg branch "$BRANCH" \ + '{ + id: $id, + timestamp: $ts, + prompt: $prompt, + session: $session, + branch: $branch, + pr_number: null, + pr_url: null, + status: "pending" + }' >> "$LOG_FILE" + +exit 0 diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh new file mode 100755 index 0000000..b1d220b --- /dev/null +++ b/.claude/hooks/session-start.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Claude Code — SessionStart hook +# +# Injects recent request history into Claude's context at session start. +# stdout is appended to Claude's context window. +# +# Input (stdin): JSON with fields: session_id, cwd, session_start_source + +set -euo pipefail + +INPUT=$(cat) +CWD=$(echo "$INPUT" | jq -r '.cwd // "$PWD"') + +LOG_FILE="${CWD}/.claude/request-log.jsonl" + +if [ ! -f "$LOG_FILE" ] || [ ! -s "$LOG_FILE" ]; then + exit 0 +fi + +TOTAL=$(wc -l < "$LOG_FILE" | tr -d ' ') +OPEN=$(jq -r 'select(.status == "open")' "$LOG_FILE" 2>/dev/null | jq -s 'length') + +echo "=== Forgejo Workflow: Session Context ===" +echo "Request log: .claude/request-log.jsonl ($TOTAL total, $OPEN open PRs)" +echo "" + +# Last 5 entries +echo "Recent requests:" +tail -5 "$LOG_FILE" | jq -r ' + " [\(.timestamp[0:16])] \(.prompt | gsub("\n"; " ") | .[0:72])" + + "\n branch: \(.branch)" + + (if .pr_number then " | PR #\(.pr_number) (\(.status))" else " | no PR yet" end) +' 2>/dev/null || true + +echo "" +echo "Run '.claude/scripts/show-history.sh' for full history." +echo "=========================================" + +exit 0 diff --git a/.claude/request-log.jsonl b/.claude/request-log.jsonl new file mode 100644 index 0000000..e69de29 diff --git a/.claude/scripts/create-pr.sh b/.claude/scripts/create-pr.sh new file mode 100755 index 0000000..0de184e --- /dev/null +++ b/.claude/scripts/create-pr.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# Create a PR on Forgejo via tea and record it in the request log. +# +# Usage: +# .claude/scripts/create-pr.sh [body] [base] +# +# Environment: +# FORGEJO_BASE_BRANCH — override default base branch (default: main) + +set -euo pipefail + +TITLE="${1:-}" +BODY="${2:-}" +BASE="${3:-${FORGEJO_BASE_BRANCH:-main}}" +LOG_FILE=".claude/request-log.jsonl" + +if [ -z "$TITLE" ]; then + echo "Usage: create-pr.sh <title> [body] [base-branch]" >&2 + exit 1 +fi + +if ! command -v tea &>/dev/null; then + cat >&2 <<'MSG' +Error: 'tea' CLI not found. + +Install options: + nix develop path:~/.claude/workflows/forgejo # recommended + brew install gitea/tap/tea # macOS + go install gitea.com/gitea/tea@latest # Go + +Then configure: tea login add +MSG + exit 1 +fi + +CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) + +if [ "$CURRENT_BRANCH" = "$BASE" ]; then + echo "Error: currently on base branch '$BASE'. Create a feature branch first." >&2 + exit 1 +fi + +echo "Creating PR: '$TITLE'" +echo " head: $CURRENT_BRANCH → base: $BASE" +echo "" + +# Build tea args +TEA_ARGS=(pr create + --title "$TITLE" + --head "$CURRENT_BRANCH" + --base "$BASE" +) + +if [ -n "$BODY" ]; then + TEA_ARGS+=(--description "$BODY") +fi + +PR_OUTPUT=$(tea "${TEA_ARGS[@]}" 2>&1) +echo "$PR_OUTPUT" + +# Extract URL — tea prints something like: "Created pull request #42" +# and/or a URL line +PR_URL=$(echo "$PR_OUTPUT" | grep -oE 'https?://[^[:space:]]+/pulls/[0-9]+' | head -1 || true) +PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$' || true) + +if [ -z "$PR_NUMBER" ]; then + # Fallback: parse "Created pull request #N" + PR_NUMBER=$(echo "$PR_OUTPUT" | grep -oE '#[0-9]+' | grep -oE '[0-9]+' | head -1 || true) +fi + +# Update most recent pending entry in the log +if [ -n "$PR_NUMBER" ] && [ -f "$LOG_FILE" ] && [ -s "$LOG_FILE" ]; then + TMPFILE=$(mktemp) + + # Find and update the last entry that matches current branch & is pending + awk -v branch="$CURRENT_BRANCH" -v pr_num="$PR_NUMBER" -v pr_url="$PR_URL" ' + BEGIN { updated = 0 } + { + lines[NR] = $0 + } + END { + # Walk backwards to find last matching entry + for (i = NR; i >= 1; i--) { + if (!updated && lines[i] ~ ("\"branch\":\"" branch "\"") && lines[i] ~ "\"status\":\"pending\"") { + # Use a simple substitution — jq would be cleaner but we avoid a subshell per line + cmd = "echo " lines[i] " | jq --arg pn \"" pr_num "\" --arg pu \"" pr_url "\" \".pr_number = ($pn | tonumber) | .pr_url = $pu | .status = \\\"open\\\"\"" + cmd | getline updated_line + close(cmd) + lines[i] = updated_line + updated = 1 + } + } + for (i = 1; i <= NR; i++) print lines[i] + } + ' "$LOG_FILE" > "$TMPFILE" + + mv "$TMPFILE" "$LOG_FILE" + echo "" + echo "Request log updated with PR #$PR_NUMBER" +fi diff --git a/.claude/scripts/show-history.sh b/.claude/scripts/show-history.sh new file mode 100755 index 0000000..5191d63 --- /dev/null +++ b/.claude/scripts/show-history.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Display the Claude request history for this project. +# +# Usage: +# .claude/scripts/show-history.sh [--filter open|merged|pending] [--last N] [--json] + +set -euo pipefail + +LOG_FILE=".claude/request-log.jsonl" +FILTER="" +LAST=0 +JSON_MODE=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --filter) FILTER="$2"; shift 2 ;; + --last) LAST="$2"; shift 2 ;; + --json) JSON_MODE=true; shift ;; + -*) echo "Unknown flag: $1" >&2; exit 1 ;; + *) LOG_FILE="$1"; shift ;; + esac +done + +if [ ! -f "$LOG_FILE" ]; then + echo "No request log found at $LOG_FILE" + exit 0 +fi + +if ! command -v jq &>/dev/null; then + echo "Error: jq not found. Run: nix develop path:~/.claude/workflows/forgejo" >&2 + exit 1 +fi + +TOTAL=$(wc -l < "$LOG_FILE" | tr -d ' ') + +if [ "$TOTAL" -eq 0 ]; then + echo "No requests logged yet." + exit 0 +fi + +# Build jq filter +JQ_SELECT='.' +if [ -n "$FILTER" ]; then + JQ_SELECT="select(.status == \"$FILTER\")" +fi + +CONTENT=$(jq -r "$JQ_SELECT" "$LOG_FILE" 2>/dev/null | jq -s '.') + +if [ "$LAST" -gt 0 ]; then + CONTENT=$(echo "$CONTENT" | jq ".[-${LAST}:]") +fi + +if [ "$JSON_MODE" = true ]; then + echo "$CONTENT" + exit 0 +fi + +COUNT=$(echo "$CONTENT" | jq 'length') + +echo "━━━ Request History ($LOG_FILE) ━━━" +echo "Total: $TOTAL entries | Showing: $COUNT${FILTER:+ ($FILTER)}" +echo "" + +echo "$CONTENT" | jq -r '.[] | + "[\(.timestamp[0:16])] \(.id)", + " Prompt: \(.prompt | gsub("\n";" ") | .[0:80])\(if (.prompt | length) > 80 then "…" else "" end)", + " Branch: \(.branch)", + (if .pr_number then + " PR: #\(.pr_number) [\(.status)] \(.pr_url // "")" + else + " PR: none yet (status: \(.status))" + end), + "" +' + +# Summary stats +OPEN=$(jq -r 'select(.status == "open")' "$LOG_FILE" | jq -s 'length') +MERGED=$(jq -r 'select(.status == "merged")' "$LOG_FILE" | jq -s 'length') +PENDING=$(jq -r 'select(.status == "pending")' "$LOG_FILE" | jq -s 'length') + +echo "━━━ Summary ━━━" +echo " Open PRs: $OPEN" +echo " Merged: $MERGED" +echo " No PR yet: $PENDING" diff --git a/.claude/scripts/update-entry.sh b/.claude/scripts/update-entry.sh new file mode 100755 index 0000000..c4ad64c --- /dev/null +++ b/.claude/scripts/update-entry.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Update a specific log entry (e.g. mark a PR as merged). +# +# Usage: +# .claude/scripts/update-entry.sh <req-id> --status merged +# .claude/scripts/update-entry.sh <req-id> --pr-number 42 --pr-url https://... + +set -euo pipefail + +REQ_ID="${1:-}" +LOG_FILE=".claude/request-log.jsonl" +NEW_STATUS="" +NEW_PR_NUMBER="" +NEW_PR_URL="" + +shift || true +while [[ $# -gt 0 ]]; do + case "$1" in + --status) NEW_STATUS="$2"; shift 2 ;; + --pr-number) NEW_PR_NUMBER="$2"; shift 2 ;; + --pr-url) NEW_PR_URL="$2"; shift 2 ;; + *) echo "Unknown arg: $1" >&2; exit 1 ;; + esac +done + +if [ -z "$REQ_ID" ]; then + echo "Usage: update-entry.sh <req-id> [--status STATUS] [--pr-number N] [--pr-url URL]" >&2 + exit 1 +fi + +if [ ! -f "$LOG_FILE" ]; then + echo "Log file not found: $LOG_FILE" >&2 + exit 1 +fi + +TMPFILE=$(mktemp) + +JQ_ARGS=() +JQ_FILTER='.' + +if [ -n "$NEW_STATUS" ]; then + JQ_ARGS+=(--arg status "$NEW_STATUS") + JQ_FILTER="$JQ_FILTER | .status = \$status" +fi + +if [ -n "$NEW_PR_NUMBER" ]; then + JQ_ARGS+=(--argjson pr_num "$NEW_PR_NUMBER") + JQ_FILTER="$JQ_FILTER | .pr_number = \$pr_num" +fi + +if [ -n "$NEW_PR_URL" ]; then + JQ_ARGS+=(--arg pr_url "$NEW_PR_URL") + JQ_FILTER="$JQ_FILTER | .pr_url = \$pr_url" +fi + +while IFS= read -r line; do + ID=$(echo "$line" | jq -r '.id // ""') + if [ "$ID" = "$REQ_ID" ]; then + echo "$line" | jq "${JQ_ARGS[@]}" "$JQ_FILTER" + else + echo "$line" + fi +done < "$LOG_FILE" > "$TMPFILE" + +mv "$TMPFILE" "$LOG_FILE" +echo "Updated entry $REQ_ID" diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..5ff1966 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,29 @@ +{ + "hooks": { + "UserPromptSubmit": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "bash \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/log-request.sh", + "timeout": 10, + "async": true + } + ] + } + ], + "SessionStart": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "bash \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh", + "timeout": 10 + } + ] + } + ] + } +} diff --git a/.gitignore b/.gitignore index 0982eca..1765c9a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,9 @@ ios/ *.local .DS_Store .claude/worktrees/ +e2e/test-results/ +e2e/report/ +e2e/.features-gen/ + +# Claude Code local settings (secrets, personal config) +.claude/settings.local.json diff --git a/CLAUDE.md b/CLAUDE.md index 31bbc4e..815051c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -798,3 +798,123 @@ function getItemsInContainer(containerId: string): Item[] { return allItems.filter(i => i.storageContainerId === containerId); } ``` + +--- + +## Forgejo PR Workflow + +Every substantive session is tracked in `.claude/request-log.jsonl` and backed by a Forgejo PR. + +### When to create a branch + PR + +Create a branch and PR for: +- New features (`feat/`) +- Bug fixes (`fix/`) +- Refactors (`refactor/`) +- Dependency or config changes (`chore/`) + +Skip branching/PR for: +- Questions, explanations, code reading +- Trivial one-liner fixes that are best committed directly to the current branch + +### Branch naming + +``` +feat/YYYYMMDD-short-kebab-description +fix/YYYYMMDD-short-kebab-description +chore/YYYYMMDD-short-kebab-description +refactor/YYYYMMDD-short-kebab-description +``` + +Check the current branch first: +```bash +git rev-parse --abbrev-ref HEAD +``` + +If already on a feature branch from this session, continue on it. Only create a new branch for a new distinct task. + +### Workflow for each task + +1. **Create branch** (if needed): + ```bash + git checkout -b feat/$(date +%Y%m%d)-short-description + ``` + +2. **Do the work** — write code, run tests, iterate. + +3. **Commit** with conventional commit messages: + ```bash + git add <files> + git commit -m "feat: short description" + ``` + +4. **Push**: + ```bash + git push -u origin HEAD + ``` + +5. **Create PR**: + ```bash + bash .claude/scripts/create-pr.sh \ + "feat: short description" \ + "$(cat <<'EOF' + ## Summary + + - What was done and why + + ## Test plan + + - [ ] Manual test steps + + 🤖 Generated with Claude Code + EOF + )" + ``` + + This also updates `.claude/request-log.jsonl` with the PR number. + +6. **Commit the updated request log** (as part of the PR or separately): + ```bash + git add .claude/request-log.jsonl + git commit -m "chore: log request #<req-id>" + ``` + +### Show history + +When the user asks for history, recent work, or what's been done: + +```bash +bash .claude/scripts/show-history.sh +bash .claude/scripts/show-history.sh --filter open +bash .claude/scripts/show-history.sh --last 10 +``` + +### tea CLI quick reference + +```bash +tea pr list # list open PRs +tea pr view <number> # view a PR +tea pr merge <number> # merge a PR +tea issue create # create an issue +tea repo info # show repo details +tea login list # show configured Forgejo instances +``` + +### Request log format + +`.claude/request-log.jsonl` — one JSON object per line, git-tracked: + +```json +{ + "id": "req_20260226_143012_a3f9b", + "timestamp": "2026-02-26T14:30:12Z", + "prompt": "Add dark mode support", + "session": "abc123", + "branch": "feat/20260226-dark-mode", + "pr_number": 42, + "pr_url": "https://forge.example.com/user/repo/pulls/42", + "status": "open" +} +``` + +Possible statuses: `pending` (no PR yet), `open`, `merged`, `closed`.