docs: add GitHub wiki + scripts/wiki.sh helper with secret-scan

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) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-04-20 15:31:19 -07:00
parent 76bfeb34d4
commit d2a447fcc4
4 changed files with 285 additions and 0 deletions
+4
View File
@@ -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
+23
View File
@@ -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.
+4
View File
@@ -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:
+254
View File
@@ -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 <Page-Name> # create Page-Name.md with stub template
# ./scripts/wiki.sh stub-check # list pages still containing the TODO stub
# ./scripts/wiki.sh commit "<msg>" # secret-scan, then git add -A && git commit
# ./scripts/wiki.sh push # secret-scan again, then git push
# ./scripts/wiki.sh touch <Page-Name> # 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 <Page-Name>"
# 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" <<EOF
# ${title}
${STUB_MARKER} This page is a stub. See [Wiki Maintenance](Wiki-Maintenance).
---
_Last updated: $(today) — stub_
EOF
log "Created $path"
}
cmd_stub_check() {
need_worktree
# Wiki-Maintenance.md documents the stub template in a code fence, so it
# would always match the marker — exempt it.
local matches count
matches="$(cd "$WIKI_DIR" && grep -rlnF --exclude-dir=.git --exclude="$MAINTENANCE_PAGE" -- "$STUB_MARKER" . 2>/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 <Page-Name>"
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 "<message>" [--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