bootstrap: projects management repo
This commit is contained in:
0
.projects.lock
Normal file
0
.projects.lock
Normal file
18
PROJECTS_INDEX.md
Normal file
18
PROJECTS_INDEX.md
Normal 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
45
README.md
Normal 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 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).
|
||||
4
projects.json
Normal file
4
projects.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"last_updated": "2026-02-28",
|
||||
"projects": []
|
||||
}
|
||||
124
tools/projectsctl
Executable file
124
tools/projectsctl
Executable 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
54
tools/render_projects_index.py
Executable 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")
|
||||
Reference in New Issue
Block a user