Files
Reza Rezvani 2f57ef8948 feat(agenthub): add AgentHub plugin with cross-domain examples, SEO optimization, and docs site fixes
- AgentHub: 13 files updated with non-engineering examples (content drafts,
  research, strategy) — engineering stays primary, cross-domain secondary
- AgentHub: 7 slash commands, 5 Python scripts, 3 references, 1 agent,
  dry_run.py validation (57 checks)
- Marketplace: agenthub entry added with cross-domain keywords, engineering
  POWERFUL updated (25→30), product (12→13), counts synced across all configs
- SEO: generate-docs.py now produces keyword-rich <title> tags and meta
  descriptions using SKILL.md frontmatter — "Claude Code Skills" in site_name
  propagates to all 276 HTML pages
- SEO: per-domain title suffixes (Agent Skill for Codex & OpenClaw, etc.),
  slug-as-title cleanup, domain label stripping from titles
- Broken links: 141→0 warnings — new rewrite_skill_internal_links() converts
  references/, scripts/, assets/ links to GitHub source URLs; skills/index.md
  phantom slugs fixed (6 marketing, 7 RA/QM)
- Counts synced: 204 skills, 266 tools, 382 refs, 16 agents, 17 commands,
  21 plugins — consistent across CLAUDE.md, README.md, docs/index.md,
  marketplace.json, getting-started.md, mkdocs.yml
- Platform sync: Codex 163 skills, Gemini 246 items, OpenClaw compatible

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 12:10:46 +01:00

303 lines
9.8 KiB
Python

#!/usr/bin/env python3
"""AgentHub session state machine and lifecycle manager.
Manages session states (init → running → evaluating → merged/archived),
lists sessions, and handles cleanup of worktrees and branches.
Usage:
python session_manager.py --list
python session_manager.py --status 20260317-143022
python session_manager.py --update 20260317-143022 --state running
python session_manager.py --cleanup 20260317-143022
python session_manager.py --demo
"""
import argparse
import json
import os
import subprocess
import sys
from datetime import datetime, timezone
SESSIONS_PATH = ".agenthub/sessions"
VALID_STATES = ["init", "running", "evaluating", "merged", "archived"]
VALID_TRANSITIONS = {
"init": ["running"],
"running": ["evaluating"],
"evaluating": ["merged", "archived"],
"merged": [],
"archived": [],
}
def load_state(session_id):
"""Load session state.json."""
state_path = os.path.join(SESSIONS_PATH, session_id, "state.json")
if not os.path.exists(state_path):
return None
with open(state_path) as f:
return json.load(f)
def save_state(session_id, state):
"""Save session state.json."""
state_path = os.path.join(SESSIONS_PATH, session_id, "state.json")
state["updated"] = datetime.now(timezone.utc).isoformat()
with open(state_path, "w") as f:
json.dump(state, f, indent=2)
f.write("\n")
def load_config(session_id):
"""Load session config.yaml (simple key: value parsing)."""
config_path = os.path.join(SESSIONS_PATH, session_id, "config.yaml")
if not os.path.exists(config_path):
return None
config = {}
with open(config_path) as f:
for line in f:
line = line.strip()
if ":" in line and not line.startswith("#"):
key, val = line.split(":", 1)
config[key.strip()] = val.strip().strip('"')
return config
def run_git(*args):
"""Run a git command and return stdout."""
try:
result = subprocess.run(
["git"] + list(args),
capture_output=True, text=True, check=True
)
return result.stdout.strip()
except subprocess.CalledProcessError:
return ""
def list_sessions(output_format="text"):
"""List all sessions with their states."""
if not os.path.isdir(SESSIONS_PATH):
print("No sessions found. Run hub_init.py first.")
return
sessions = []
for sid in sorted(os.listdir(SESSIONS_PATH)):
session_dir = os.path.join(SESSIONS_PATH, sid)
if not os.path.isdir(session_dir):
continue
state = load_state(sid)
config = load_config(sid)
if state and config:
sessions.append({
"session_id": sid,
"state": state.get("state", "unknown"),
"task": config.get("task", ""),
"agents": config.get("agent_count", "?"),
"created": state.get("created", ""),
})
if output_format == "json":
print(json.dumps({"sessions": sessions}, indent=2))
return
if not sessions:
print("No sessions found.")
return
print("AgentHub Sessions")
print()
header = f"{'SESSION ID':<20} {'STATE':<12} {'AGENTS':<8} {'TASK'}"
print(header)
print("-" * 70)
for s in sessions:
task = s["task"][:40] + "..." if len(s["task"]) > 40 else s["task"]
print(f"{s['session_id']:<20} {s['state']:<12} {s['agents']:<8} {task}")
def show_status(session_id, output_format="text"):
"""Show detailed status for a session."""
state = load_state(session_id)
config = load_config(session_id)
if not state or not config:
print(f"Error: Session {session_id} not found", file=sys.stderr)
sys.exit(1)
if output_format == "json":
print(json.dumps({"config": config, "state": state}, indent=2))
return
print(f"Session: {session_id}")
print(f" State: {state.get('state', 'unknown')}")
print(f" Task: {config.get('task', '')}")
print(f" Agents: {config.get('agent_count', '?')}")
print(f" Base branch: {config.get('base_branch', '?')}")
if config.get("eval_cmd"):
print(f" Eval: {config['eval_cmd']}")
if config.get("metric"):
print(f" Metric: {config['metric']} ({config.get('direction', '?')})")
print(f" Created: {state.get('created', '?')}")
print(f" Updated: {state.get('updated', '?')}")
# Show agent branches
branches = run_git("branch", "--list", f"hub/{session_id}/*",
"--format=%(refname:short)")
if branches:
print()
print(" Branches:")
for b in branches.split("\n"):
if b.strip():
print(f" {b.strip()}")
def update_state(session_id, new_state):
"""Transition session to a new state."""
state = load_state(session_id)
if not state:
print(f"Error: Session {session_id} not found", file=sys.stderr)
sys.exit(1)
current = state.get("state", "unknown")
if new_state not in VALID_STATES:
print(f"Error: Invalid state '{new_state}'. "
f"Valid: {', '.join(VALID_STATES)}", file=sys.stderr)
sys.exit(1)
valid_next = VALID_TRANSITIONS.get(current, [])
if new_state not in valid_next:
print(f"Error: Cannot transition from '{current}' to '{new_state}'. "
f"Valid transitions: {', '.join(valid_next) or 'none (terminal)'}",
file=sys.stderr)
sys.exit(1)
state["state"] = new_state
save_state(session_id, state)
print(f"Session {session_id}: {current}{new_state}")
def cleanup_session(session_id):
"""Clean up worktrees and optionally archive branches."""
config = load_config(session_id)
if not config:
print(f"Error: Session {session_id} not found", file=sys.stderr)
sys.exit(1)
# Find and remove worktrees for this session
worktree_output = run_git("worktree", "list", "--porcelain")
removed = 0
if worktree_output:
current_path = None
for line in worktree_output.split("\n"):
if line.startswith("worktree "):
current_path = line[len("worktree "):]
elif line.startswith("branch ") and current_path:
ref = line[len("branch "):]
if f"hub/{session_id}/" in ref:
result = subprocess.run(
["git", "worktree", "remove", "--force", current_path],
capture_output=True, text=True
)
if result.returncode == 0:
removed += 1
print(f" Removed worktree: {current_path}")
current_path = None
print(f"Cleaned up {removed} worktrees for session {session_id}")
def run_demo():
"""Show demo output."""
print("=" * 60)
print("AgentHub Session Manager — Demo Mode")
print("=" * 60)
print()
print("--- Session List ---")
print("AgentHub Sessions")
print()
header = f"{'SESSION ID':<20} {'STATE':<12} {'AGENTS':<8} {'TASK'}"
print(header)
print("-" * 70)
print(f"{'20260317-143022':<20} {'merged':<12} {'3':<8} Optimize API response time below 100ms")
print(f"{'20260317-151500':<20} {'running':<12} {'2':<8} Refactor auth module for JWT support")
print(f"{'20260317-160000':<20} {'init':<12} {'4':<8} Implement caching strategy")
print()
print("--- Session Detail ---")
print("Session: 20260317-143022")
print(" State: merged")
print(" Task: Optimize API response time below 100ms")
print(" Agents: 3")
print(" Base branch: dev")
print(" Eval: pytest bench.py --json")
print(" Metric: p50_ms (lower)")
print(" Created: 2026-03-17T14:30:22Z")
print(" Updated: 2026-03-17T14:45:00Z")
print()
print(" Branches:")
print(" hub/20260317-143022/agent-1/attempt-1 (archived)")
print(" hub/20260317-143022/agent-2/attempt-1 (merged)")
print(" hub/20260317-143022/agent-3/attempt-1 (archived)")
print()
print("--- State Transitions ---")
print("Valid transitions:")
for state, transitions in VALID_TRANSITIONS.items():
arrow = "".join(transitions) if transitions else "(terminal)"
print(f" {state}: {arrow}")
def main():
parser = argparse.ArgumentParser(
description="AgentHub session state machine and lifecycle manager"
)
parser.add_argument("--list", action="store_true",
help="List all sessions with state")
parser.add_argument("--status", type=str, metavar="SESSION_ID",
help="Show detailed session status")
parser.add_argument("--update", type=str, metavar="SESSION_ID",
help="Update session state")
parser.add_argument("--state", type=str,
help="New state for --update")
parser.add_argument("--cleanup", type=str, metavar="SESSION_ID",
help="Remove worktrees and clean up session")
parser.add_argument("--format", choices=["text", "json"], default="text",
help="Output format (default: text)")
parser.add_argument("--demo", action="store_true",
help="Show demo output")
args = parser.parse_args()
if args.demo:
run_demo()
return
if args.list:
list_sessions(args.format)
return
if args.status:
show_status(args.status, args.format)
return
if args.update:
if not args.state:
print("Error: --update requires --state", file=sys.stderr)
sys.exit(1)
update_state(args.update, args.state)
return
if args.cleanup:
cleanup_session(args.cleanup)
return
parser.print_help()
if __name__ == "__main__":
main()