chore: add Claude Project Knowledge sync tooling
- scripts/sync-claude-docs.sh with sync/check/list/help subcommands - scripts/install-claude-sync-hooks.sh for one-time hook setup - .githooks/post-commit auto-syncs on dev-doc changes - .githooks/pre-push warns (non-blocking) on stale sync - .claude-sync.conf lists 11 synced documents - SYNC_MANIFEST.md provides drift-detection anchor for Claude Chat - package.json: npm run sync:docs | sync:check - .gitignore excludes .claude-sync/ output directory Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
11
.claude-sync.conf
Normal file
11
.claude-sync.conf
Normal file
@@ -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
|
||||
54
.githooks/post-commit
Executable file
54
.githooks/post-commit
Executable file
@@ -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 <<EOF
|
||||
${configured}
|
||||
EOF
|
||||
done <<EOF
|
||||
${changed}
|
||||
EOF
|
||||
|
||||
if [ "${match}" -eq 1 ]; then
|
||||
bash "${SYNC_SCRIPT}" sync
|
||||
fi
|
||||
|
||||
exit 0
|
||||
29
.githooks/pre-push
Executable file
29
.githooks/pre-push
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Warn (non-blocking) if .claude-sync/ is stale relative to current source files.
|
||||
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
if [ -z "${REPO_ROOT}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
cd "${REPO_ROOT}"
|
||||
|
||||
SYNC_SCRIPT="scripts/sync-claude-docs.sh"
|
||||
if [ ! -f "${SYNC_SCRIPT}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
set +e
|
||||
bash "${SYNC_SCRIPT}" check >/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
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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/
|
||||
|
||||
8
package.json
Normal file
8
package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
40
scripts/README.md
Normal file
40
scripts/README.md
Normal file
@@ -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.
|
||||
27
scripts/install-claude-sync-hooks.sh
Executable file
27
scripts/install-claude-sync-hooks.sh
Executable file
@@ -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
|
||||
308
scripts/sync-claude-docs.sh
Executable file
308
scripts/sync-claude-docs.sh
Executable file
@@ -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' "<!-- CREWLI-SYNC"
|
||||
printf 'source: %s\n' "${p}"
|
||||
printf 'sha256: %s\n' "${hash}"
|
||||
printf 'git-sha: %s\n' "${git_short_sha}"
|
||||
printf 'synced-at: %s\n' "${synced_at}"
|
||||
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
|
||||
Reference in New Issue
Block a user