commit 9cb7a67331913b59ed5931360675aea2d2dbd81c Author: OpenClaw Bot Date: Sat Feb 28 12:04:34 2026 -0600 bootstrap: projects management repo diff --git a/.projects.lock b/.projects.lock new file mode 100644 index 0000000..e69de29 diff --git a/PROJECTS_INDEX.md b/PROJECTS_INDEX.md new file mode 100644 index 0000000..23b533b --- /dev/null +++ b/PROJECTS_INDEX.md @@ -0,0 +1,18 @@ +# Projects Index + +This file is generated from projects.json. Do not edit manually. + +Last Updated: 2026-02-28 + +## Active Projects + +- (none) + +## Paused Projects + +- (none) + +## Archived Projects + +- (none) + diff --git a/README.md b/README.md new file mode 100644 index 0000000..cfc0273 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +Projects Management Repo (auto-updated by OpenClaw) + +Goal +- OpenClaw updates this repo automatically (on main) to keep a durable project index. +- Humans read it offline. Do not hand-edit PROJECTS_INDEX.md. + +Files +- projects.json Source of truth (machine-edited) +- PROJECTS_INDEX.md Generated from projects.json (human-readable) +- tools/projectsctl The ONLY allowed writer (adds/updates + commits + pushes) +- tools/render_projects_index.py Generator/validator + +Prereqs (Debian/Ubuntu) +- sudo apt-get update +- sudo apt-get install -y git jq python3 util-linux + +Bootstrap +1) Generate the index: + ./tools/projectsctl render + +2) Add a project (example): + PROJREPO=$(pwd) ./tools/projectsctl add "CognitiveOS" active \ + "https://git.molloyhome.net/mmolloy/Cognition-OS" \ + "/home/openclaw/.openclaw/workspace/repo-Cognition-OS" \ + "Deterministic microkernel-style control plane for LLM/agent systems." + +3) Change status: + PROJREPO=$(pwd) ./tools/projectsctl set-status "CognitiveOS" paused + +4) Update notes: + PROJREPO=$(pwd) ./tools/projectsctl update-notes "CognitiveOS" "New notes here..." + +Making OpenClaw read this reliably +- Symlink PROJECTS_INDEX.md into OpenClaw’s indexed memory folder: + + ln -sf /opt/projects/projects-mgmt/PROJECTS_INDEX.md ~/.openclaw/memory/projects_index.md + sudo systemctl restart openclaw-gateway + +Then confirm it’s indexed: + sqlite3 ~/.openclaw/memory/main.sqlite "SELECT path FROM files WHERE path LIKE '%projects_index%';" + +Operational contract (recommended) +- OpenClaw MUST use tools/projectsctl for any changes. +- Never edit PROJECTS_INDEX.md directly. +- If push fails, OpenClaw must report the error (no silent drift). diff --git a/projects.json b/projects.json new file mode 100644 index 0000000..c1c17bc --- /dev/null +++ b/projects.json @@ -0,0 +1,4 @@ +{ + "last_updated": "2026-02-28", + "projects": [] +} \ No newline at end of file diff --git a/tools/projectsctl b/tools/projectsctl new file mode 100755 index 0000000..9fc4513 --- /dev/null +++ b/tools/projectsctl @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO="${PROJREPO:-$(cd "$(dirname "$0")/.." && pwd)}" +LOCK="$REPO/.projects.lock" +TS="$(date +%F_%H%M%S)" +cd "$REPO" + +need() { command -v "$1" >/dev/null 2>&1 || { echo "ERROR: missing $1"; exit 1; }; } +need jq +need git +need python3 +need flock + +exec 9>"$LOCK" +flock -n 9 || { echo "ERROR: projectsctl is already running"; exit 2; } + +backup() { + mkdir -p "$REPO/backups" + cp -a "$REPO/projects.json" "$REPO/backups/projects.json.$TS" +} + +render() { + ./tools/render_projects_index.py >/dev/null +} + +git_sync() { + git rev-parse --is-inside-work-tree >/dev/null + git checkout main >/dev/null 2>&1 || true + git pull --rebase --autostash >/dev/null 2>&1 || true +} + +git_commit_push() { + if git diff --quiet && git diff --cached --quiet; then + echo "No changes to commit." + exit 0 + fi + git add projects.json PROJECTS_INDEX.md + git commit -m "projects: update ($TS)" >/dev/null + git push origin main >/dev/null + echo "OK: committed and pushed to main." +} + +cmd="${1:-}"; shift || true + +case "$cmd" in + add) + name="${1:-}"; status="${2:-active}"; shift 2 || true + repo="${1:-}"; local_path="${2:-}"; shift 2 || true + notes="${*:-}" + [[ -n "$name" ]] || { echo "usage: projectsctl add [status] [repo] [local_path] [notes...]"; exit 1; } + + git_sync + backup + + tmp="$(mktemp)" + jq --arg name "$name" --arg status "$status" --arg repo "$repo" --arg lp "$local_path" --arg notes "$notes" ' + .projects |= ( . + [ {name:$name,status:$status,type:"",repo:$repo,local_path:$lp,notes:$notes} ] ) + ' projects.json > "$tmp" + mv "$tmp" projects.json + + render + git_commit_push + ;; + + set-status) + name="${1:-}"; status="${2:-}" + [[ -n "$name" && -n "$status" ]] || { echo "usage: projectsctl set-status "; exit 1; } + + git_sync + backup + + tmp="$(mktemp)" + jq --arg name "$name" --arg status "$status" ' + .projects |= (map(if .name==$name then .status=$status else . end)) + ' projects.json > "$tmp" + mv "$tmp" projects.json + + render + git_commit_push + ;; + + update-notes) + name="${1:-}"; shift || true + notes="${*:-}" + [[ -n "$name" && -n "$notes" ]] || { echo "usage: projectsctl update-notes "; exit 1; } + + git_sync + backup + + tmp="$(mktemp)" + jq --arg name "$name" --arg notes "$notes" ' + .projects |= (map(if .name==$name then .notes=$notes else . end)) + ' projects.json > "$tmp" + mv "$tmp" projects.json + + render + git_commit_push + ;; + + list) + python3 - <<'PY' +import json +p=json.load(open("projects.json")) +for pr in p.get("projects",[]): + print(f"{pr.get('status',''):8} {pr.get('name','')}") +PY + ;; + + render) + render + echo "OK: rendered PROJECTS_INDEX.md" + ;; + + *) + echo "usage:" + echo " projectsctl add [status] [repo] [local_path] [notes...]" + echo " projectsctl set-status " + echo " projectsctl update-notes " + echo " projectsctl list" + echo " projectsctl render" + exit 1 + ;; +esac diff --git a/tools/render_projects_index.py b/tools/render_projects_index.py new file mode 100755 index 0000000..f5cb56a --- /dev/null +++ b/tools/render_projects_index.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +import json, datetime, sys + +p = json.load(open("projects.json")) +today = datetime.date.today().isoformat() + +def norm(s): return (s or "").strip() + +projects = p.get("projects", []) +if not isinstance(projects, list): + print("ERROR: projects.json must contain a top-level 'projects' array", file=sys.stderr) + sys.exit(2) + +for i, pr in enumerate(projects): + if not isinstance(pr, dict): + raise SystemExit(f"ERROR: project[{i}] is not an object") + if not norm(pr.get("name")): + raise SystemExit(f"ERROR: project[{i}] missing name") + st = norm(pr.get("status")) + if st not in ("active","paused","archived"): + raise SystemExit(f"ERROR: invalid status '{st}' for {pr.get('name')} (allowed: active|paused|archived)") + +p["last_updated"] = today + +def section(title, items): + out = [f"## {title}\n\n"] + if not items: + out.append("- (none)\n\n") + return out + for pr in sorted(items, key=lambda x: x["name"].lower()): + out.append(f"- Project: {pr['name']}\n") + out.append(f" Status: {pr.get('status','')}\n") + if pr.get("type"): out.append(f" Type: {pr['type']}\n") + if pr.get("repo"): out.append(f" Repo: {pr['repo']}\n") + if pr.get("local_path"): out.append(f" Local Path: {pr['local_path']}\n") + if pr.get("notes"): out.append(f" Notes: {pr['notes']}\n") + out.append("\n") + return out + +active = [x for x in projects if x.get("status") == "active"] +paused = [x for x in projects if x.get("status") == "paused"] +archived = [x for x in projects if x.get("status") == "archived"] + +md = [] +md.append("# Projects Index\n\n") +md.append("This file is generated from projects.json. Do not edit manually.\n\n") +md.append(f"Last Updated: {p['last_updated']}\n\n") +md += section("Active Projects", active) +md += section("Paused Projects", paused) +md += section("Archived Projects", archived) + +open("PROJECTS_INDEX.md","w").write("".join(md)) +json.dump(p, open("projects.json","w"), indent=2, sort_keys=False) +print("OK: rendered PROJECTS_INDEX.md")