From d2a447fcc420b6061969498b1da49c3fe7e78122 Mon Sep 17 00:00:00 2001 From: Alan Wizemann Date: Mon, 20 Apr 2026 15:31:19 -0700 Subject: [PATCH] docs: add GitHub wiki + scripts/wiki.sh helper with secret-scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Public docs now live at https://github.com/awizemann/scarf/wiki (separate git repo cloned to .wiki-worktree/, mirroring the .gh-pages-worktree/ pattern). Internal dev notes stay in scarf/docs/. scripts/wiki.sh wraps pull/commit/push with a two-pass secret-scan: hard patterns (token regexes + private-key headers + a user-maintained scripts/wiki-blocklist.txt) abort with non-zero exit; soft assignment patterns (api_key=…, password=…, token=…) warn and require --force-terms. CLAUDE.md gains a Wiki section listing the update triggers (new feature, new service, architecture change, Hermes version bump, full release, keyboard/sidebar change) and the workflow. CONTRIBUTING.md points external contributors at the wiki Edit button or a direct clone. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 4 + CLAUDE.md | 23 +++++ CONTRIBUTING.md | 4 + scripts/wiki.sh | 254 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 285 insertions(+) create mode 100755 scripts/wiki.sh diff --git a/.gitignore b/.gitignore index 04bc48b..2585a80 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Xcode build/ .gh-pages-worktree/ +.wiki-worktree/ DerivedData/ *.pbxuser !default.pbxuser @@ -52,3 +53,6 @@ scarf/standards/backups/ # history. RELEASE_NOTES.md stays tracked (committed with the version bump). releases/v*/*.zip releases/v*/appcast-entry.xml + +# Wiki helper: personal patterns (hostnames, IPs) blocked from the wiki push. +scripts/wiki-blocklist.txt diff --git a/CLAUDE.md b/CLAUDE.md index 8360682..4414b98 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,6 +59,29 @@ The script bumps version, archives Universal (arm64 + x86_64) + ARM64-only varia **Prerequisites (one-time, already set up on Alan's machine):** Developer ID Application cert in login Keychain (team `3Q6X2L86C4`), notarytool keychain profile `scarf-notary`, Sparkle EdDSA private key in Keychain item `https://sparkle-project.org`, `gh-pages` branch + GitHub Pages enabled. See the header of [scripts/release.sh](scripts/release.sh) and the Releases section in [README.md](README.md) for details. +## Wiki + +Public documentation lives in the GitHub wiki at https://github.com/awizemann/scarf/wiki. The wiki is a separate git repo cloned to `.wiki-worktree/` in the repo root (gitignored, sibling to `.gh-pages-worktree/`). Internal dev notes stay in `scarf/docs/`; the wiki is for public-facing reference. + +**Update the wiki when:** +- A new feature module is added under `scarf/scarf/scarf/Features/` → extend the relevant User Guide page. +- A new core service is added under `Core/Services/` → extend `Core-Services.md`. +- Architecture changes (AppCoordinator, transport, MVVM-F rule, sandbox) → `Architecture-Overview.md` + the specific sub-page. +- Hermes version bumps in this file → `Hermes-Version-Compatibility.md`. +- `scripts/release.sh` completes a full (non-draft) release → bump latest-version on `Home.md` + append to `Release-Notes-Index.md`. +- Keyboard shortcut or sidebar section changes → `Keyboard-Shortcuts.md` / `Sidebar-and-Navigation.md`. + +**Skip for:** bug fixes with no user-observable change, pure refactors, typos, test-only changes, internal cleanups. + +```bash +./scripts/wiki.sh pull # always first +# edit .wiki-worktree/*.md with normal tools +./scripts/wiki.sh commit "docs: describe X" # runs secret-scan +./scripts/wiki.sh push # runs secret-scan again, then push +``` + +**Never** commit API keys, tokens, `.env` files, private keys, or real hostnames/IPs to the wiki. The script's two-pass secret-scan blocks common token patterns and a user-maintained blocklist at `scripts/wiki-blocklist.txt` (gitignored). Do not bypass without explicit approval. Full workflow on the wiki itself at `.wiki-worktree/Wiki-Maintenance.md`. + ## Hermes Version Targets Hermes v0.9.0 (v2026.4.13). Log lines may carry an optional `[session_id]` tag between the level and logger name — `HermesLogService.parseLine` treats the session tag as an optional capture group, so older untagged lines still parse. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c73bff0..395b3ec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,6 +33,10 @@ Rules: - The app only reads from `~/.hermes/state.db` (never writes). Memory files are the exception. - Swift 6 strict concurrency: `@MainActor` default isolation, `nonisolated` for service methods. +## Documentation + +Public docs live in the [GitHub wiki](https://github.com/awizemann/scarf/wiki). Small fixes (typos, clarifications) can be made via the "Edit" button on any wiki page — you need push access to the main repo. For larger changes, clone the wiki locally (`git clone git@github.com:awizemann/scarf.wiki.git`) or open an issue describing the proposed change. + ## Reporting Issues Open an issue with: diff --git a/scripts/wiki.sh b/scripts/wiki.sh new file mode 100755 index 0000000..8ee0f78 --- /dev/null +++ b/scripts/wiki.sh @@ -0,0 +1,254 @@ +#!/usr/bin/env bash +# +# Scarf wiki helper — wraps the local .wiki-worktree/ clone with a secret-scan. +# +# Usage: +# ./scripts/wiki.sh status # git status inside .wiki-worktree/ +# ./scripts/wiki.sh pull # fetch + fast-forward; aborts if dirty +# ./scripts/wiki.sh new # create Page-Name.md with stub template +# ./scripts/wiki.sh stub-check # list pages still containing the TODO stub +# ./scripts/wiki.sh commit "" # secret-scan, then git add -A && git commit +# ./scripts/wiki.sh push # secret-scan again, then git push +# ./scripts/wiki.sh touch # bump "Last updated" line to today +# ./scripts/wiki.sh --help # this help +# +# The secret-scan has two tiers: +# - Hard patterns (tokens, keys, private-key headers): block with non-zero exit. +# - Soft keywords (password, api_key, secret, bearer, authorization:, .env): +# warn and require --force-terms on commit/push to proceed. +# +# A user-maintained blocklist lives at scripts/wiki-blocklist.txt (gitignored). +# One pattern per line — blank lines and lines starting with # are ignored. +# Matches are treated as HARD blocks. Use this for personal IPs, hostnames, etc. +# +# Bootstrap (one-time): +# 1. In GitHub repo Settings → Features → Wikis, enable Wikis and restrict +# editing to collaborators. +# 2. Visit https://github.com/awizemann/scarf/wiki and save any first page +# via the UI (this is what creates the underlying .wiki.git repo). +# 3. From repo root: +# git clone git@github.com:awizemann/scarf.wiki.git .wiki-worktree +# +# Recovery: if .wiki-worktree/ is deleted, re-run step 3. Remote is authoritative. + +set -euo pipefail + +# ---------- config ---------- +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +WIKI_DIR="$REPO_ROOT/.wiki-worktree" +WIKI_REMOTE="git@github.com:awizemann/scarf.wiki.git" +BLOCKLIST="$REPO_ROOT/scripts/wiki-blocklist.txt" +STUB_MARKER='> **TODO: document.**' +MAINTENANCE_PAGE="Wiki-Maintenance.md" + +# ---------- helpers ---------- +log() { printf '\033[1;34m==> %s\033[0m\n' "$*"; } +warn() { printf '\033[1;33m[WARN] %s\033[0m\n' "$*" >&2; } +die() { printf '\033[1;31m[ERR] %s\033[0m\n' "$*" >&2; exit 1; } + +need_worktree() { + [[ -d "$WIKI_DIR/.git" ]] || die "no wiki clone at $WIKI_DIR + Run: git clone $WIKI_REMOTE .wiki-worktree + (See --help for bootstrap steps.)" +} + +in_wiki() { (cd "$WIKI_DIR" && "$@"); } + +today() { date +%Y-%m-%d; } + +# ---------- secret-scan ---------- +# +# scan_hard: exits non-zero if any hard pattern or user-blocklist pattern matches. +# scan_soft: warns (returns 1) if any soft keyword matches, else 0. The caller +# decides whether to bail based on --force-terms. +# +# All scans ignore the .git directory of the wiki clone. + +hard_regex='(sk-[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9]{30,}|ghs_[A-Za-z0-9]{30,}|ghu_[A-Za-z0-9]{30,}|gho_[A-Za-z0-9]{30,}|ghr_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|-----BEGIN [A-Z ]*PRIVATE KEY-----|BEGIN OPENSSH PRIVATE KEY)' +# Soft tier targets KEY=value / KEY: value patterns where the key name suggests +# a secret. Catches accidental .env paste; ignores legitimate mentions of the +# words "password" / "secret" in prose. +soft_regex='([Pp]assword|[Aa]pi[_-]?[Kk]ey|[Ss]ecret[_-][Kk]ey|[Tt]oken|[Aa]uth[_-]?[Tt]oken|[Bb]earer)[[:space:]]*[=:][[:space:]]*['"'"'"]?[A-Za-z0-9_+./-]{8,}' + +scan_hard() { + local hits + hits="$(cd "$WIKI_DIR" && grep -rInE --exclude-dir=.git "$hard_regex" . 2>/dev/null || true)" + if [[ -n "$hits" ]]; then + printf '%s\n' "$hits" >&2 + die "hard-pattern secret match — aborting. Remove the above and retry." + fi + + if [[ -f "$BLOCKLIST" ]]; then + local pat + while IFS= read -r pat; do + [[ -z "$pat" || "$pat" =~ ^# ]] && continue + local blocklist_hits + blocklist_hits="$(cd "$WIKI_DIR" && grep -rInF --exclude-dir=.git -- "$pat" . 2>/dev/null || true)" + if [[ -n "$blocklist_hits" ]]; then + printf '%s\n' "$blocklist_hits" >&2 + die "user blocklist match on pattern \"$pat\" — aborting." + fi + done < "$BLOCKLIST" + fi +} + +scan_soft() { + local hits + # Exempt the maintenance page (it documents the forbidden terms on purpose). + hits="$(cd "$WIKI_DIR" && grep -rInE --exclude-dir=.git --exclude="$MAINTENANCE_PAGE" "$soft_regex" . 2>/dev/null || true)" + if [[ -n "$hits" ]]; then + printf '%s\n' "$hits" >&2 + warn "soft-keyword matches above. Review carefully. Pass --force-terms to proceed." + return 1 + fi + return 0 +} + +# ---------- commands ---------- +cmd_status() { + need_worktree + in_wiki git status +} + +cmd_pull() { + need_worktree + if [[ -n "$(in_wiki git status --porcelain)" ]]; then + die "wiki has uncommitted changes — commit or stash before pulling." + fi + log "Pulling wiki" + in_wiki git fetch origin + in_wiki git merge --ff-only origin/master + log "Up to date." +} + +cmd_new() { + need_worktree + local name="${1:-}" + [[ -n "$name" ]] || die "usage: wiki.sh new " + # Normalize spaces → dashes; strip .md if the user added it. + name="${name%.md}" + name="${name// /-}" + local path="$WIKI_DIR/${name}.md" + if [[ -e "$path" ]]; then + die "already exists: $path" + fi + local title="${name//-/ }" + cat > "$path" </dev/null || true)" + if [[ -z "$matches" ]]; then + log "No stub pages remain." + return 0 + fi + count="$(printf '%s\n' "$matches" | wc -l | tr -d ' ')" + log "$count stub page(s):" + printf '%s\n' "$matches" | sed 's|^\./||' | sort +} + +cmd_touch() { + need_worktree + local name="${1:-}" + [[ -n "$name" ]] || die "usage: wiki.sh touch " + name="${name%.md}" + name="${name// /-}" + local path="$WIKI_DIR/${name}.md" + [[ -f "$path" ]] || die "no such page: $path" + # Replace the YYYY-MM-DD portion of the Last updated line, keep whatever trails it. + # Uses sed -i '' for BSD/macOS compatibility. + sed -i '' -E "s/(_Last updated: )[0-9]{4}-[0-9]{2}-[0-9]{2}/\1$(today)/" "$path" + log "Touched ${name}.md → $(today)" +} + +cmd_commit() { + need_worktree + local msg="" force=0 + for arg in "$@"; do + case "$arg" in + --force-terms) force=1 ;; + -*) die "unknown flag: $arg" ;; + *) [[ -z "$msg" ]] && msg="$arg" || die "unexpected arg: $arg" ;; + esac + done + [[ -n "$msg" ]] || die 'usage: wiki.sh commit "" [--force-terms]' + log "Secret-scan (hard patterns + blocklist)" + scan_hard + log "Secret-scan (soft keywords)" + if ! scan_soft; then + if [[ "$force" -eq 1 ]]; then + warn "proceeding past soft-keyword matches (--force-terms)" + else + die "soft-keyword matches — re-run with --force-terms to proceed." + fi + fi + in_wiki git add -A + if [[ -z "$(in_wiki git status --porcelain)" ]]; then + warn "nothing to commit" + return 0 + fi + in_wiki git commit -m "$msg" + log "Committed: $msg" +} + +cmd_push() { + need_worktree + local force=0 + for arg in "$@"; do + case "$arg" in + --force-terms) force=1 ;; + *) die "unknown arg: $arg" ;; + esac + done + log "Secret-scan before push (hard patterns + blocklist)" + scan_hard + log "Secret-scan before push (soft keywords)" + if ! scan_soft; then + if [[ "$force" -eq 1 ]]; then + warn "proceeding past soft-keyword matches (--force-terms)" + else + die "soft-keyword matches — re-run with --force-terms to proceed." + fi + fi + # Nothing to push? + local ahead + ahead="$(in_wiki git rev-list --count @{u}..HEAD 2>/dev/null || echo 0)" + if [[ "$ahead" -eq 0 ]]; then + warn "nothing to push (0 commits ahead of origin)" + return 0 + fi + log "Pushing $ahead commit(s) to origin" + in_wiki git push origin HEAD + log "Pushed. Verify at https://github.com/awizemann/scarf/wiki" +} + +cmd_help() { + sed -n '2,32p' "$0" | sed 's/^# \{0,1\}//' +} + +# ---------- dispatch ---------- +sub="${1:-}" +shift || true +case "$sub" in + status) cmd_status "$@" ;; + pull) cmd_pull "$@" ;; + new) cmd_new "$@" ;; + stub-check) cmd_stub_check "$@" ;; + touch) cmd_touch "$@" ;; + commit) cmd_commit "$@" ;; + push) cmd_push "$@" ;; + -h|--help|help|"") cmd_help ;; + *) die "unknown subcommand: $sub (run --help)" ;; +esac