diff --git a/.claude-sync.conf b/.claude-sync.conf new file mode 100644 index 00000000..39db72b9 --- /dev/null +++ b/.claude-sync.conf @@ -0,0 +1,11 @@ +CLAUDE.md +.cursorrules +dev-docs/SCHEMA.md +dev-docs/API.md +dev-docs/ARCH-FORM-BUILDER.md +dev-docs/AUTH_ARCHITECTURE.md +dev-docs/VUEXY_COMPONENTS.md +dev-docs/BACKLOG.md +dev-docs/SECURITY_AUDIT.md +dev-docs/design-document.md +dev-docs/UX_SPEC_FESTIVAL_HIERARCHY.md diff --git a/.githooks/post-commit b/.githooks/post-commit new file mode 100755 index 00000000..4dfa10c0 --- /dev/null +++ b/.githooks/post-commit @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Auto-sync Claude Project Knowledge when a commit touched any configured doc. + +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)" +if [ -z "${REPO_ROOT}" ]; then + exit 0 +fi +cd "${REPO_ROOT}" + +CONF_FILE=".claude-sync.conf" +SYNC_SCRIPT="scripts/sync-claude-docs.sh" + +if [ ! -f "${CONF_FILE}" ] || [ ! -f "${SYNC_SCRIPT}" ]; then + exit 0 +fi + +# Files changed in the commit we just made. +changed="$(git diff-tree --no-commit-id --name-only -r HEAD 2>/dev/null || true)" +if [ -z "${changed}" ]; then + exit 0 +fi + +# Configured paths (strip # comments and blanks). +configured="$(awk ' + { + sub(/#.*/, "") + gsub(/^[[:space:]]+|[[:space:]]+$/, "") + if (length($0) > 0) print + } +' "${CONF_FILE}")" + +match=0 +while IFS= read -r cfile; do + [ -z "${cfile}" ] && continue + while IFS= read -r conf_path; do + [ -z "${conf_path}" ] && continue + if [ "${cfile}" = "${conf_path}" ]; then + match=1 + break 2 + fi + done </dev/null 2>&1 +rc=$? +set -e + +if [ "${rc}" -ne 0 ]; then + { + printf '%s\n' "⚠️ .claude-sync/ is stale relative to current dev-docs." + printf '%s\n' " Fix: bash scripts/sync-claude-docs.sh sync" + printf '%s\n' " (push is NOT blocked)" + } >&2 +fi + +exit 0 diff --git a/.gitignore b/.gitignore index 9679787c..236a2c0c 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,6 @@ docs/.vitepress/cache # Local deploy wrapper (per-developer SSH alias) /run-deploy-from-local.sh !/run-deploy-from-local.example.sh + +# Claude Project Knowledge sync output (regenerated by scripts/sync-claude-docs.sh) +.claude-sync/ diff --git a/package.json b/package.json new file mode 100644 index 00000000..1c8d7e8d --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "name": "crewli", + "private": true, + "scripts": { + "sync:docs": "bash scripts/sync-claude-docs.sh sync", + "sync:check": "bash scripts/sync-claude-docs.sh check" + } +} diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 00000000..14454f92 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,40 @@ +# Scripts + +Repo-level helper scripts. + +## Claude Project Knowledge sync + +Keeps the Crewli dev-docs in Claude Project Knowledge aligned with the repo. + +### One-time setup + +```bash +bash scripts/install-claude-sync-hooks.sh +``` + +This points git at `.githooks/` and marks the hooks executable. + +### Manual trigger + +```bash +npm run sync:docs # regenerate .claude-sync/ +npm run sync:check # verify .claude-sync/ matches dev-docs +``` + +The underlying script also supports `list` and `help`: + +```bash +bash scripts/sync-claude-docs.sh list +bash scripts/sync-claude-docs.sh help +``` + +### Automatic + +- `post-commit`: syncs automatically when any file in `.claude-sync.conf` is committed +- `pre-push`: warns if `.claude-sync/` is stale before pushing (non-blocking) + +### After sync + +Upload everything in `.claude-sync/` (including `SYNC_MANIFEST.md`) to the +Crewli Claude Project Knowledge, replacing existing versions. The manifest is +what Claude Chat uses for drift detection against the current HEAD. diff --git a/scripts/install-claude-sync-hooks.sh b/scripts/install-claude-sync-hooks.sh new file mode 100755 index 00000000..a3942ad7 --- /dev/null +++ b/scripts/install-claude-sync-hooks.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +# One-time installer for the Claude Project Knowledge sync hooks. +# Points git at the versioned .githooks/ directory and ensures hooks are executable. + +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)" +if [ -z "${REPO_ROOT}" ]; then + printf '%s\n' "❌ Not inside a git repository." >&2 + exit 1 +fi +cd "${REPO_ROOT}" + +if [ ! -d ".githooks" ]; then + printf '%s\n' "❌ .githooks/ directory missing at repo root." >&2 + exit 1 +fi + +git config core.hooksPath .githooks +chmod +x .githooks/post-commit .githooks/pre-push + +cat <<'EOF' +✅ Claude sync hooks installed. + • post-commit: auto-syncs on dev-doc changes + • pre-push: warns if .claude-sync/ is stale +Run once to verify: bash scripts/sync-claude-docs.sh sync +EOF diff --git a/scripts/sync-claude-docs.sh b/scripts/sync-claude-docs.sh new file mode 100755 index 00000000..d095f56a --- /dev/null +++ b/scripts/sync-claude-docs.sh @@ -0,0 +1,308 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Claude Project Knowledge sync tool. +# Keeps .claude-sync/ aligned with documents listed in .claude-sync.conf +# so the bundle can be uploaded to Claude Chat Project Knowledge. + +err() { printf '%s\n' "$*" >&2; } + +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)" +if [ -z "${REPO_ROOT}" ]; then + err "❌ Not inside a git repository." + exit 1 +fi +cd "${REPO_ROOT}" + +CONF_FILE=".claude-sync.conf" +OUT_DIR=".claude-sync" +MANIFEST="${OUT_DIR}/SYNC_MANIFEST.md" + +sha256() { + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + elif command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + err "❌ Neither shasum nor sha256sum is available." + exit 1 + fi +} + +iso_now() { + # ISO-8601 with timezone offset; portable across macOS (BSD) and Linux (GNU). + date +"%Y-%m-%dT%H:%M:%S%z" +} + +file_mtime() { + # Returns "YYYY-MM-DD HH:MM:SS" for the given file. + local f="$1" + if stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$f" >/dev/null 2>&1; then + stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$f" + else + stat -c "%y" "$f" | cut -d'.' -f1 + fi +} + +read_conf_paths() { + # Emits configured paths, skipping blank lines and # comments; trims whitespace. + if [ ! -f "${CONF_FILE}" ]; then + err "❌ Config file not found: ${CONF_FILE}" + exit 1 + fi + awk ' + { + sub(/#.*/, "") + gsub(/^[[:space:]]+|[[:space:]]+$/, "") + if (length($0) > 0) print + } + ' "${CONF_FILE}" +} + +cmd_sync() { + local paths=() + while IFS= read -r line; do + paths+=("${line}") + done < <(read_conf_paths) + + if [ ${#paths[@]} -eq 0 ]; then + err "❌ No paths configured in ${CONF_FILE}." + exit 1 + fi + + # Verify every listed file exists. + local missing=() + local p + for p in "${paths[@]}"; do + if [ ! -f "${p}" ]; then + missing+=("${p}") + fi + done + if [ ${#missing[@]} -gt 0 ]; then + err "❌ Missing source files listed in ${CONF_FILE}:" + for p in "${missing[@]}"; do + err " ${p}" + done + exit 1 + fi + + # Detect filename collisions (e.g. two README.md under different folders). + local seen_basenames="" + for p in "${paths[@]}"; do + local bn + bn="$(basename "${p}")" + # Find any prior path with the same basename. + local other="" + local q + for q in "${paths[@]}"; do + if [ "${q}" = "${p}" ]; then continue; fi + if [ "$(basename "${q}")" = "${bn}" ]; then + other="${q}" + break + fi + done + if [ -n "${other}" ]; then + case "${seen_basenames}" in + *"|${bn}|"*) ;; + *) + err "❌ Filename collision in ${CONF_FILE}: basename '${bn}' used by:" + err " ${p}" + err " ${other}" + exit 1 + ;; + esac + seen_basenames="${seen_basenames}|${bn}|" + fi + done + + rm -rf "${OUT_DIR}" + mkdir -p "${OUT_DIR}" + + local git_full_sha git_short_sha branch synced_at + git_full_sha="$(git rev-parse HEAD)" + git_short_sha="$(git rev-parse --short HEAD)" + branch="$(git rev-parse --abbrev-ref HEAD)" + synced_at="$(iso_now)" + + # Build manifest rows as we go. + local manifest_rows="" + local count=0 + + for p in "${paths[@]}"; do + local bn hash mtime dest + bn="$(basename "${p}")" + hash="$(sha256 "${p}")" + mtime="$(file_mtime "${p}")" + dest="${OUT_DIR}/${bn}" + + { + printf '%s\n' "" + printf '\n' + } > "${dest}" + cat "${p}" >> "${dest}" + + manifest_rows="${manifest_rows}| ${bn} | ${p} | ${hash} | ${mtime} |"$'\n' + count=$((count + 1)) + done + + { + printf '%s\n' "# Crewli Project Knowledge Sync Manifest" + printf '\n' + printf '**Generated:** %s\n' "${synced_at}" + printf '**Git SHA:** %s (short: %s)\n' "${git_full_sha}" "${git_short_sha}" + printf '**Branch:** %s\n' "${branch}" + printf '**Files synced:** %d\n' "${count}" + printf '\n' + printf '%s\n' "## Upload instructions" + printf '\n' + printf '%s\n' "Upload every file in \`.claude-sync/\` — including this manifest — to the" + printf '%s\n' "Crewli Claude Project Knowledge, replacing existing versions. The manifest" + printf '%s\n' "is what Claude Chat uses for drift detection against the current HEAD." + printf '\n' + printf '%s\n' "## Drift-detection rule" + printf '\n' + printf '%s\n' "Re-sync is required when:" + printf '%s\n' "1. Current git HEAD SHA differs from this manifest's git SHA, AND" + printf '%s\n' "2. Any listed file's current SHA256 differs from the hash recorded below." + printf '\n' + printf '%s\n' "## File manifest" + printf '\n' + printf '%s\n' "| File | Source path | SHA256 | Source mtime |" + printf '%s\n' "|------|-------------|--------|--------------|" + printf '%s' "${manifest_rows}" + printf '\n' + printf '%s\n' "## Workflow note for Claude Chat" + printf '\n' + printf '%s\n' "When the user reports work that touched any listed file, verify the" + printf '%s\n' "manifest SHA above matches the latest HEAD they reference. If not," + printf '%s\n' "require a re-sync before generating new prompts." + } > "${MANIFEST}" + + err "✅ Claude sync complete — ${count} files ready in ${OUT_DIR}/" + err "📤 Upload to Claude Project Knowledge (replace existing):" + for p in "${paths[@]}"; do + err " ${OUT_DIR}/$(basename "${p}")" + done + err " ${MANIFEST}" +} + +cmd_check() { + if [ ! -f "${MANIFEST}" ]; then + err "❌ No sync manifest found. Run: bash scripts/sync-claude-docs.sh sync" + exit 1 + fi + + local paths=() + while IFS= read -r line; do + paths+=("${line}") + done < <(read_conf_paths) + + local drift=() + local missing=() + local p + for p in "${paths[@]}"; do + if [ ! -f "${p}" ]; then + missing+=("${p}") + continue + fi + # Extract recorded SHA from manifest row whose "Source path" column matches p. + local recorded + recorded="$(awk -F' *\\| *' -v src="${p}" ' + $0 ~ /^\|/ && $3 == src { print $4; exit } + ' "${MANIFEST}")" + if [ -z "${recorded}" ]; then + drift+=("${p} (not recorded in manifest)") + continue + fi + local current + current="$(sha256 "${p}")" + if [ "${current}" != "${recorded}" ]; then + drift+=("${p} (source changed since last sync)") + fi + done + + if [ ${#missing[@]} -gt 0 ]; then + err "⚠️ Out of sync:" + for p in "${missing[@]}"; do + err " ${p} (source file missing)" + done + fi + + if [ ${#drift[@]} -gt 0 ]; then + if [ ${#missing[@]} -eq 0 ]; then + err "⚠️ Out of sync:" + fi + for p in "${drift[@]}"; do + err " ${p}" + done + err "Run: bash scripts/sync-claude-docs.sh sync" + exit 1 + fi + + if [ ${#missing[@]} -gt 0 ]; then + err "Run: bash scripts/sync-claude-docs.sh sync" + exit 1 + fi + + printf '%s\n' "✅ .claude-sync/ matches current dev-docs" +} + +cmd_list() { + local paths=() + while IFS= read -r line; do + paths+=("${line}") + done < <(read_conf_paths) + + local p marker + for p in "${paths[@]}"; do + if [ -f "${p}" ]; then + marker="✓" + else + marker="✗" + fi + printf ' %s %s\n' "${marker}" "${p}" + done +} + +cmd_help() { + cat <<'EOF' +sync-claude-docs.sh — Crewli → Claude Project Knowledge sync + +Usage: + bash scripts/sync-claude-docs.sh [subcommand] + +Subcommands: + sync (default) Rebuild .claude-sync/ from .claude-sync.conf and write SYNC_MANIFEST.md + check Verify .claude-sync/ matches current source files; non-zero exit on drift + list Print configured source paths with existence markers (✓ / ✗) + help Show this help text + +Examples: + bash scripts/sync-claude-docs.sh + bash scripts/sync-claude-docs.sh sync + bash scripts/sync-claude-docs.sh check + bash scripts/sync-claude-docs.sh list + +Config: .claude-sync.conf (one path per line; # comments and blank lines OK) +Output: .claude-sync/ (git-ignored; regenerated on each sync) +EOF +} + +sub="${1:-sync}" +case "${sub}" in + sync) cmd_sync ;; + check) cmd_check ;; + list) cmd_list ;; + help|-h|--help) cmd_help ;; + *) + err "❌ Unknown subcommand: ${sub}" + err "" + cmd_help >&2 + exit 1 + ;; +esac