bootstrap: projects management repo

This commit is contained in:
OpenClaw Bot
2026-02-28 12:04:34 -06:00
commit 9cb7a67331
6 changed files with 245 additions and 0 deletions

0
.projects.lock Normal file
View File

18
PROJECTS_INDEX.md Normal file
View File

@@ -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)

45
README.md Normal file
View File

@@ -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 OpenClaws indexed memory folder:
ln -sf /opt/projects/projects-mgmt/PROJECTS_INDEX.md ~/.openclaw/memory/projects_index.md
sudo systemctl restart openclaw-gateway
Then confirm its 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).

4
projects.json Normal file
View File

@@ -0,0 +1,4 @@
{
"last_updated": "2026-02-28",
"projects": []
}

124
tools/projectsctl Executable file
View File

@@ -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 <name> [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 <name> <active|paused|archived>"; 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 <name> <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 <name> [status] [repo] [local_path] [notes...]"
echo " projectsctl set-status <name> <active|paused|archived>"
echo " projectsctl update-notes <name> <notes...>"
echo " projectsctl list"
echo " projectsctl render"
exit 1
;;
esac

54
tools/render_projects_index.py Executable file
View File

@@ -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")