aboutsummaryrefslogtreecommitdiff
path: root/.ai/scripts/cross-agent-comms/cross-agent-status
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-06 21:59:52 -0500
committerCraig Jennings <c@cjennings.net>2026-05-06 21:59:52 -0500
commitd81b23ad6b6e437dfe3c338a00a4be39bc555146 (patch)
tree2d4b0d7890fd1fc70d81282b81fed2808c28a106 /.ai/scripts/cross-agent-comms/cross-agent-status
parent201377f57430ef28d02e703a2191434bbee55c75 (diff)
downloadrulesets-d81b23ad6b6e437dfe3c338a00a4be39bc555146.tar.gz
rulesets-d81b23ad6b6e437dfe3c338a00a4be39bc555146.zip
chore(ai): initialize project notes and Claude tooling surfaces
Replace the seed notes.org with project-specific context (layout, install modes, task tracker location, recent inflection point). Bring in the synced template surfaces (protocols, workflows, scripts, references, retrospectives, someday-maybe) as tracked content for this content/documentation project.
Diffstat (limited to '.ai/scripts/cross-agent-comms/cross-agent-status')
-rwxr-xr-x.ai/scripts/cross-agent-comms/cross-agent-status185
1 files changed, 185 insertions, 0 deletions
diff --git a/.ai/scripts/cross-agent-comms/cross-agent-status b/.ai/scripts/cross-agent-comms/cross-agent-status
new file mode 100755
index 0000000..4eee75b
--- /dev/null
+++ b/.ai/scripts/cross-agent-comms/cross-agent-status
@@ -0,0 +1,185 @@
+#!/usr/bin/env python3
+"""Point-in-time snapshot of pending cross-agent messages across local projects.
+
+See cross-agent-status.md. Pending = messages in inbox/from-agents/ whose
+CONVERSATION_ID has no MESSAGE_TYPE: release at a later #+TIMESTAMP.
+
+HALT: prints a prominent banner before normal output, but continues to enumerate.
+"""
+
+from __future__ import annotations
+
+import argparse
+import glob
+import json
+import os
+import re
+import sys
+from pathlib import Path
+
+CONFIG_DIR = Path.home() / ".config" / "cross-agent-comms"
+HALT_FILE = CONFIG_DIR / "HALT"
+DEFAULT_GLOB = str(Path.home() / "projects" / "*" / "inbox" / "from-agents") + "/"
+
+
+def parse_frontmatter(path: Path) -> dict[str, str]:
+ try:
+ text = path.read_text()
+ except OSError:
+ return {}
+ fm: dict[str, str] = {}
+ for line in text.splitlines():
+ line = line.rstrip()
+ if not line:
+ if fm:
+ break
+ continue
+ m = re.match(r"#\+([A-Z_]+):\s*(.*)", line)
+ if m:
+ fm[m.group(1)] = m.group(2).strip()
+ elif fm:
+ break
+ return fm
+
+
+def project_name_from_path(path: str) -> str:
+ """Walk up from path to find ~/projects/<name>/..."""
+ home = str(Path.home())
+ parts = Path(path).parts
+ for i, part in enumerate(parts):
+ if part == "projects" and i + 1 < len(parts) and str(Path(*parts[: i + 1])) == os.path.join(home, "projects"):
+ return parts[i + 1]
+ # Fallback: dir three levels up from the .org file (project/inbox/from-agents/file.org)
+ return Path(path).parent.parent.parent.name
+
+
+def scan_project(inbox_dir: Path) -> tuple[int, str | None, int | None]:
+ """Return (pending_count, most_recent_filename_or_None, most_recent_age_seconds_or_None)."""
+ if not inbox_dir.is_dir():
+ return 0, None, None
+
+ # Group .org files by CONVERSATION_ID, also collect release timestamps per conv.
+ org_files = sorted(inbox_dir.glob("*.org"))
+ if not org_files:
+ return 0, None, None
+
+ by_conv: dict[str, list[tuple[str, str, Path]]] = {} # conv_id -> [(timestamp, msg_type, path)]
+ for f in org_files:
+ fm = parse_frontmatter(f)
+ conv = fm.get("CONVERSATION_ID")
+ ts = fm.get("TIMESTAMP")
+ mt = fm.get("MESSAGE_TYPE")
+ if not conv or not ts or not mt:
+ # Malformed file: count as pending under conv "_unparseable".
+ by_conv.setdefault("_unparseable", []).append(("", "request", f))
+ continue
+ by_conv.setdefault(conv, []).append((ts, mt, f))
+
+ pending_files: list[Path] = []
+ for conv, entries in by_conv.items():
+ entries.sort(key=lambda e: e[0])
+ # Find the latest release timestamp.
+ release_ts = None
+ for ts, mt, _f in entries:
+ if mt == "release" and (release_ts is None or ts > release_ts):
+ release_ts = ts
+ for ts, mt, f in entries:
+ if mt == "release":
+ continue
+ if release_ts is not None and ts <= release_ts:
+ continue
+ pending_files.append(f)
+
+ if not pending_files:
+ return 0, None, None
+
+ # Most-recent by mtime (proxy for arrival order).
+ most_recent = max(pending_files, key=lambda p: p.stat().st_mtime)
+ import time
+ age = int(time.time() - most_recent.stat().st_mtime)
+ return len(pending_files), most_recent.name, age
+
+
+def fmt_age(seconds: int | None) -> str:
+ if seconds is None:
+ return "—"
+ if seconds < 60:
+ return f"{seconds}s ago"
+ if seconds < 3600:
+ return f"{seconds // 60} min ago"
+ if seconds < 86400:
+ return f"{seconds // 3600} hr ago"
+ return f"{seconds // 86400} day(s) ago"
+
+
+def render_banner_if_halt() -> None:
+ if not HALT_FILE.exists():
+ return
+ try:
+ reason = HALT_FILE.read_text().strip()
+ except OSError:
+ reason = "(HALT file unreadable; treated as halted)"
+ print("⚠ HALT ACTIVE — cross-agent comms paused")
+ if reason:
+ print(f" reason: {reason}")
+ print(f" clear: rm {HALT_FILE} (or: cross-agent-resume)")
+ print()
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description="Snapshot of pending cross-agent messages across local projects.")
+ parser.add_argument("--json", action="store_true", help="Emit JSON output")
+ parser.add_argument("--projects-glob", default=DEFAULT_GLOB,
+ help=f"Glob for project from-agents dirs (default: {DEFAULT_GLOB})")
+ args = parser.parse_args()
+
+ render_banner_if_halt()
+
+ matched = sorted(glob.glob(args.projects_glob))
+ rows = []
+ for path in matched:
+ inbox = Path(path)
+ if not inbox.is_dir():
+ continue
+ proj = project_name_from_path(path)
+ count, most_recent, age = scan_project(inbox)
+ rows.append({
+ "name": proj,
+ "pending_count": count,
+ "most_recent": (
+ {"filename": most_recent, "age_seconds": age}
+ if most_recent else None
+ ),
+ })
+
+ # Sort: pending-first, then alphabetical by name.
+ rows.sort(key=lambda r: (-r["pending_count"], r["name"]))
+
+ if args.json:
+ import datetime as _dt
+ payload = {
+ "scanned_at": _dt.datetime.now(_dt.timezone.utc).isoformat(),
+ "halt_active": HALT_FILE.exists(),
+ "projects": rows,
+ }
+ print(json.dumps(payload, indent=2))
+ return 0
+
+ if not rows:
+ print("No projects with inbox/from-agents/ found — 0 pending.")
+ return 0
+
+ # Human-readable table.
+ name_w = max(len("project"), max(len(r["name"]) for r in rows))
+ print(f"{'project':<{name_w}} pending most-recent")
+ for r in rows:
+ most_recent_str = "—"
+ if r["most_recent"]:
+ most_recent_str = f"{r['most_recent']['filename']} ({fmt_age(r['most_recent']['age_seconds'])})"
+ print(f"{r['name']:<{name_w}} {r['pending_count']:<7} {most_recent_str}")
+
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())