Files
claude-skills-reference/engineering/agenthub/scripts/board_manager.py
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

267 lines
8.4 KiB
Python

#!/usr/bin/env python3
"""AgentHub message board manager.
CRUD operations for the agent message board: list channels, read posts,
create new posts, and reply to threads.
Usage:
python board_manager.py --list
python board_manager.py --read dispatch
python board_manager.py --post --channel results --author agent-1 --message "Task complete"
python board_manager.py --thread 001-agent-1 --message "Additional details"
python board_manager.py --demo
"""
import argparse
import json
import os
import re
import sys
from datetime import datetime, timezone
BOARD_PATH = ".agenthub/board"
def get_board_path():
"""Get the board directory path."""
if not os.path.isdir(BOARD_PATH):
print(f"Error: Board not found at {BOARD_PATH}. Run hub_init.py first.",
file=sys.stderr)
sys.exit(1)
return BOARD_PATH
def load_index():
"""Load the board index."""
index_path = os.path.join(get_board_path(), "_index.json")
if not os.path.exists(index_path):
return {"channels": ["dispatch", "progress", "results"], "counters": {}}
with open(index_path) as f:
return json.load(f)
def save_index(index):
"""Save the board index."""
index_path = os.path.join(get_board_path(), "_index.json")
with open(index_path, "w") as f:
json.dump(index, f, indent=2)
f.write("\n")
def list_channels(output_format="text"):
"""List all board channels with post counts."""
index = load_index()
channels = []
for ch in index.get("channels", []):
ch_path = os.path.join(get_board_path(), ch)
count = 0
if os.path.isdir(ch_path):
count = len([f for f in os.listdir(ch_path)
if f.endswith(".md")])
channels.append({"channel": ch, "posts": count})
if output_format == "json":
print(json.dumps({"channels": channels}, indent=2))
else:
print("Board Channels:")
print()
for ch in channels:
print(f" {ch['channel']:<15} {ch['posts']} posts")
def parse_post_frontmatter(content):
"""Parse YAML frontmatter from a post."""
metadata = {}
body = content
if content.startswith("---"):
parts = content.split("---", 2)
if len(parts) >= 3:
fm = parts[1].strip()
body = parts[2].strip()
for line in fm.split("\n"):
if ":" in line:
key, val = line.split(":", 1)
metadata[key.strip()] = val.strip()
return metadata, body
def read_channel(channel, output_format="text"):
"""Read all posts in a channel."""
ch_path = os.path.join(get_board_path(), channel)
if not os.path.isdir(ch_path):
print(f"Error: Channel '{channel}' not found", file=sys.stderr)
sys.exit(1)
files = sorted([f for f in os.listdir(ch_path) if f.endswith(".md")])
posts = []
for fname in files:
filepath = os.path.join(ch_path, fname)
with open(filepath) as f:
content = f.read()
metadata, body = parse_post_frontmatter(content)
posts.append({
"file": fname,
"metadata": metadata,
"body": body,
})
if output_format == "json":
print(json.dumps({"channel": channel, "posts": posts}, indent=2))
else:
print(f"Channel: {channel} ({len(posts)} posts)")
print("=" * 60)
for post in posts:
author = post["metadata"].get("author", "unknown")
timestamp = post["metadata"].get("timestamp", "")
print(f"\n--- {post['file']} (by {author}, {timestamp}) ---")
print(post["body"])
def create_post(channel, author, message, parent=None):
"""Create a new post in a channel."""
ch_path = os.path.join(get_board_path(), channel)
os.makedirs(ch_path, exist_ok=True)
# Get next sequence number
index = load_index()
counters = index.get("counters", {})
seq = counters.get(channel, 0) + 1
counters[channel] = seq
index["counters"] = counters
save_index(index)
# Generate filename
timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
safe_author = re.sub(r"[^a-zA-Z0-9_-]", "", author)
filename = f"{seq:03d}-{safe_author}-{timestamp}.md"
# Build post content
lines = [
"---",
f"author: {author}",
f"timestamp: {datetime.now(timezone.utc).isoformat()}",
f"channel: {channel}",
f"sequence: {seq}",
]
if parent:
lines.append(f"parent: {parent}")
else:
lines.append("parent: null")
lines.append("---")
lines.append("")
lines.append(message)
lines.append("")
filepath = os.path.join(ch_path, filename)
with open(filepath, "w") as f:
f.write("\n".join(lines))
print(f"Posted to {channel}/{filename}")
return filename
def run_demo():
"""Show demo output."""
print("=" * 60)
print("AgentHub Board Manager — Demo Mode")
print("=" * 60)
print()
print("--- Channel List ---")
print("Board Channels:")
print()
print(" dispatch 2 posts")
print(" progress 4 posts")
print(" results 3 posts")
print()
print("--- Read Channel: results ---")
print("Channel: results (3 posts)")
print("=" * 60)
print()
print("--- 001-agent-1-20260317T143510Z.md (by agent-1, 2026-03-17T14:35:10Z) ---")
print("## Result Summary")
print()
print("- **Approach**: Added caching layer for database queries")
print("- **Files changed**: 3")
print("- **Metric**: 165ms (baseline: 180ms, delta: -15ms)")
print("- **Confidence**: Medium — 2 edge cases not covered")
print()
print("--- 002-agent-2-20260317T143645Z.md (by agent-2, 2026-03-17T14:36:45Z) ---")
print("## Result Summary")
print()
print("- **Approach**: Replaced O(n²) sort with hash map lookup")
print("- **Files changed**: 2")
print("- **Metric**: 142ms (baseline: 180ms, delta: -38ms)")
print("- **Confidence**: High — all tests pass")
print()
print("--- 003-agent-3-20260317T143422Z.md (by agent-3, 2026-03-17T14:34:22Z) ---")
print("## Result Summary")
print()
print("- **Approach**: Minor loop optimizations")
print("- **Files changed**: 1")
print("- **Metric**: 190ms (baseline: 180ms, delta: +10ms)")
print("- **Confidence**: Low — no meaningful improvement")
def main():
parser = argparse.ArgumentParser(
description="AgentHub message board manager"
)
parser.add_argument("--list", action="store_true",
help="List all channels with post counts")
parser.add_argument("--read", type=str, metavar="CHANNEL",
help="Read all posts in a channel")
parser.add_argument("--post", action="store_true",
help="Create a new post")
parser.add_argument("--channel", type=str,
help="Channel for --post or --thread")
parser.add_argument("--author", type=str,
help="Author name for --post")
parser.add_argument("--message", type=str,
help="Message content for --post or --thread")
parser.add_argument("--thread", type=str, metavar="POST_ID",
help="Reply to a post (sets parent)")
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_channels(args.format)
return
if args.read:
read_channel(args.read, args.format)
return
if args.post:
if not args.channel or not args.author or not args.message:
print("Error: --post requires --channel, --author, and --message",
file=sys.stderr)
sys.exit(1)
create_post(args.channel, args.author, args.message)
return
if args.thread:
if not args.message:
print("Error: --thread requires --message", file=sys.stderr)
sys.exit(1)
channel = args.channel or "results"
author = args.author or "coordinator"
create_post(channel, author, args.message, parent=args.thread)
return
parser.print_help()
if __name__ == "__main__":
main()