- 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>
276 lines
8.7 KiB
Python
276 lines
8.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Analyze the AgentHub git DAG.
|
|
|
|
Detects frontier branches (leaves with no children), displays DAG graphs,
|
|
and shows per-agent branch status for a session.
|
|
|
|
Usage:
|
|
python dag_analyzer.py --frontier --session 20260317-143022
|
|
python dag_analyzer.py --graph
|
|
python dag_analyzer.py --status --session 20260317-143022
|
|
python dag_analyzer.py --demo
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from datetime import datetime
|
|
|
|
|
|
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 as e:
|
|
print(f"Git error: {e.stderr.strip()}", file=sys.stderr)
|
|
return ""
|
|
|
|
|
|
def get_hub_branches(session_id=None):
|
|
"""Get all hub/* branches, optionally filtered by session."""
|
|
output = run_git("branch", "--list", "hub/*", "--format=%(refname:short)")
|
|
if not output:
|
|
return []
|
|
branches = output.strip().split("\n")
|
|
if session_id:
|
|
prefix = f"hub/{session_id}/"
|
|
branches = [b for b in branches if b.startswith(prefix)]
|
|
return branches
|
|
|
|
|
|
def get_branch_commit(branch):
|
|
"""Get the commit hash for a branch."""
|
|
return run_git("rev-parse", "--short", branch)
|
|
|
|
|
|
def get_branch_commit_count(branch, base_branch="main"):
|
|
"""Count commits ahead of base branch."""
|
|
output = run_git("rev-list", "--count", f"{base_branch}..{branch}")
|
|
try:
|
|
return int(output)
|
|
except ValueError:
|
|
return 0
|
|
|
|
|
|
def get_branch_last_commit_date(branch):
|
|
"""Get the last commit date for a branch."""
|
|
output = run_git("log", "-1", "--format=%ci", branch)
|
|
if output:
|
|
return output[:19]
|
|
return "unknown"
|
|
|
|
|
|
def get_branch_last_commit_msg(branch):
|
|
"""Get the last commit message for a branch."""
|
|
return run_git("log", "-1", "--format=%s", branch)
|
|
|
|
|
|
def detect_frontier(session_id=None):
|
|
"""Find frontier branches (tips with no child branches).
|
|
|
|
A branch is on the frontier if no other hub branch contains its tip commit
|
|
as an ancestor (i.e., it has no children in the DAG).
|
|
"""
|
|
branches = get_hub_branches(session_id)
|
|
if not branches:
|
|
return []
|
|
|
|
# Get commit hashes for all branches
|
|
branch_commits = {}
|
|
for b in branches:
|
|
commit = run_git("rev-parse", b)
|
|
if commit:
|
|
branch_commits[b] = commit
|
|
|
|
# A branch is frontier if its commit is not an ancestor of any other branch
|
|
frontier = []
|
|
for branch, commit in branch_commits.items():
|
|
is_ancestor = False
|
|
for other_branch, other_commit in branch_commits.items():
|
|
if other_branch == branch:
|
|
continue
|
|
# Check if commit is ancestor of other_commit
|
|
result = subprocess.run(
|
|
["git", "merge-base", "--is-ancestor", commit, other_commit],
|
|
capture_output=True
|
|
)
|
|
if result.returncode == 0:
|
|
is_ancestor = True
|
|
break
|
|
if not is_ancestor:
|
|
frontier.append(branch)
|
|
|
|
return frontier
|
|
|
|
|
|
def show_graph():
|
|
"""Display the git DAG graph for hub branches."""
|
|
branches = get_hub_branches()
|
|
if not branches:
|
|
print("No hub/* branches found.")
|
|
return
|
|
|
|
# Use git log with graph for hub branches
|
|
branch_args = [b for b in branches]
|
|
output = run_git(
|
|
"log", "--all", "--oneline", "--graph", "--decorate",
|
|
"--simplify-by-decoration",
|
|
*[f"--branches=hub/*"]
|
|
)
|
|
if output:
|
|
print(output)
|
|
else:
|
|
print("No hub commits found.")
|
|
|
|
|
|
def show_status(session_id, output_format="table"):
|
|
"""Show per-agent branch status for a session."""
|
|
branches = get_hub_branches(session_id)
|
|
if not branches:
|
|
print(f"No branches found for session {session_id}")
|
|
return
|
|
|
|
frontier = detect_frontier(session_id)
|
|
|
|
# Parse agent info from branch names
|
|
agents = []
|
|
for branch in sorted(branches):
|
|
# Pattern: hub/{session}/agent-{N}/attempt-{M}
|
|
match = re.match(r"hub/[^/]+/agent-(\d+)/attempt-(\d+)", branch)
|
|
if match:
|
|
agent_num = int(match.group(1))
|
|
attempt = int(match.group(2))
|
|
else:
|
|
agent_num = 0
|
|
attempt = 1
|
|
|
|
commit = get_branch_commit(branch)
|
|
commits = get_branch_commit_count(branch)
|
|
last_date = get_branch_last_commit_date(branch)
|
|
last_msg = get_branch_last_commit_msg(branch)
|
|
is_frontier = branch in frontier
|
|
|
|
agents.append({
|
|
"agent": agent_num,
|
|
"attempt": attempt,
|
|
"branch": branch,
|
|
"commit": commit,
|
|
"commits_ahead": commits,
|
|
"last_update": last_date,
|
|
"last_message": last_msg,
|
|
"frontier": is_frontier,
|
|
})
|
|
|
|
if output_format == "json":
|
|
print(json.dumps({"session": session_id, "agents": agents}, indent=2))
|
|
return
|
|
|
|
# Table output
|
|
print(f"Session: {session_id}")
|
|
print(f"Branches: {len(branches)} | Frontier: {len(frontier)}")
|
|
print()
|
|
header = f"{'AGENT':<8} {'BRANCH':<45} {'COMMITS':<8} {'STATUS':<10} {'LAST UPDATE':<20}"
|
|
print(header)
|
|
print("-" * len(header))
|
|
for a in agents:
|
|
status = "frontier" if a["frontier"] else "merged"
|
|
print(f"agent-{a['agent']:<4} {a['branch']:<45} {a['commits_ahead']:<8} {status:<10} {a['last_update']:<20}")
|
|
|
|
|
|
def run_demo():
|
|
"""Show demo output."""
|
|
print("=" * 60)
|
|
print("AgentHub DAG Analyzer — Demo Mode")
|
|
print("=" * 60)
|
|
print()
|
|
|
|
print("--- Frontier Detection ---")
|
|
print("Frontier branches (leaves with no children):")
|
|
print(" hub/20260317-143022/agent-1/attempt-1 (3 commits ahead)")
|
|
print(" hub/20260317-143022/agent-2/attempt-1 (5 commits ahead)")
|
|
print(" hub/20260317-143022/agent-3/attempt-1 (2 commits ahead)")
|
|
print()
|
|
|
|
print("--- Session Status ---")
|
|
print("Session: 20260317-143022")
|
|
print("Branches: 3 | Frontier: 3")
|
|
print()
|
|
header = f"{'AGENT':<8} {'BRANCH':<45} {'COMMITS':<8} {'STATUS':<10} {'LAST UPDATE':<20}"
|
|
print(header)
|
|
print("-" * len(header))
|
|
print(f"{'agent-1':<8} {'hub/20260317-143022/agent-1/attempt-1':<45} {'3':<8} {'frontier':<10} {'2026-03-17 14:35:10':<20}")
|
|
print(f"{'agent-2':<8} {'hub/20260317-143022/agent-2/attempt-1':<45} {'5':<8} {'frontier':<10} {'2026-03-17 14:36:45':<20}")
|
|
print(f"{'agent-3':<8} {'hub/20260317-143022/agent-3/attempt-1':<45} {'2':<8} {'frontier':<10} {'2026-03-17 14:34:22':<20}")
|
|
print()
|
|
|
|
print("--- DAG Graph ---")
|
|
print("* abc1234 (hub/20260317-143022/agent-2/attempt-1) Replaced O(n²) with hash map")
|
|
print("* def5678 Added benchmark tests")
|
|
print("| * ghi9012 (hub/20260317-143022/agent-1/attempt-1) Added caching layer")
|
|
print("| * jkl3456 Refactored data access")
|
|
print("|/")
|
|
print("| * mno7890 (hub/20260317-143022/agent-3/attempt-1) Minor optimizations")
|
|
print("|/")
|
|
print("* pqr1234 (dev) Base commit")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Analyze the AgentHub git DAG"
|
|
)
|
|
parser.add_argument("--frontier", action="store_true",
|
|
help="List frontier branches (leaves with no children)")
|
|
parser.add_argument("--graph", action="store_true",
|
|
help="Show ASCII DAG graph for hub branches")
|
|
parser.add_argument("--status", action="store_true",
|
|
help="Show per-agent branch status")
|
|
parser.add_argument("--session", type=str,
|
|
help="Filter by session ID")
|
|
parser.add_argument("--format", choices=["table", "json"], default="table",
|
|
help="Output format (default: table)")
|
|
parser.add_argument("--demo", action="store_true",
|
|
help="Show demo output")
|
|
args = parser.parse_args()
|
|
|
|
if args.demo:
|
|
run_demo()
|
|
return
|
|
|
|
if not any([args.frontier, args.graph, args.status]):
|
|
parser.print_help()
|
|
return
|
|
|
|
if args.frontier:
|
|
frontier = detect_frontier(args.session)
|
|
if args.format == "json":
|
|
print(json.dumps({"frontier": frontier}, indent=2))
|
|
else:
|
|
if frontier:
|
|
print("Frontier branches:")
|
|
for b in frontier:
|
|
print(f" {b}")
|
|
else:
|
|
print("No frontier branches found.")
|
|
print()
|
|
|
|
if args.graph:
|
|
show_graph()
|
|
print()
|
|
|
|
if args.status:
|
|
if not args.session:
|
|
print("Error: --session required with --status", file=sys.stderr)
|
|
sys.exit(1)
|
|
show_status(args.session, args.format)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|