feat: design system overhaul — sidebar, AI chats, settings, brainstorm, color cleanup
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
- Sidebar: dynamic brand-accent colors, brainstorm section restyled - AI chat general: popup panel with expand/collapse, hides when contextual AI open - AI chat contextual: tabs reordered (Actions first), X close button, height fix - Settings: all tabs restyled, 6 new color presets (sage, terracotta, iron, etc.) - Global color cleanup: emerald/orange hardcoded → brand-accent dynamic - Brainstorm page: orange → brand-accent throughout - PageEntry animation component added to key pages - Floating AI button: bg-brand-accent instead of hardcoded black - i18n: all 15 locales updated with new AI/billing keys - Billing: freemium quota tracking, BYOK, stripe subscription scaffolding - Admin: integrated into new design - AGENTS.md + CLAUDE.md project rules added
This commit is contained in:
244
.agent/skills/suno-agent-band-manager/scripts/reconcile-sidecar.py
Executable file
244
.agent/skills/suno-agent-band-manager/scripts/reconcile-sidecar.py
Executable file
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Post-unpack reconciliation helper for the Mac sidecar.
|
||||
|
||||
After `unpack-portable.sh/.ps1` extracts a sync archive on a receiving
|
||||
machine, the sidecar index.md still reflects the receiving machine's prior
|
||||
local state — even though the freshly-arrived files (WIPs, songbook entries,
|
||||
band profiles, playlist docs, session-context) may contain updates the
|
||||
sidecar narrative should integrate.
|
||||
|
||||
This script produces a punch list for the agent to walk through:
|
||||
|
||||
1. **Files modified more recently than index.md** — candidates for
|
||||
narrative integration (session history, current work, pending threads).
|
||||
2. **Validator findings** — calls `validate-sidecar.py` so drift between
|
||||
the sidecar narrative and the unpacked file state surfaces immediately.
|
||||
|
||||
The script does not edit files. The agent is responsible for reading each
|
||||
candidate and deciding whether the sidecar narrative should integrate its
|
||||
content, surfacing the decision to the user via the usual handoff
|
||||
checkpoint.
|
||||
|
||||
Usage:
|
||||
python3 scripts/reconcile-sidecar.py [project_root]
|
||||
python3 scripts/reconcile-sidecar.py --format json
|
||||
|
||||
Exit codes:
|
||||
0 — sidecar and files are in sync (or sidecar absent — nothing to check)
|
||||
1 — candidates found or validator reported errors (agent should reconcile)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _format_mtime(mtime: float) -> str:
|
||||
return datetime.fromtimestamp(mtime, tz=timezone.utc).strftime(
|
||||
"%Y-%m-%d %H:%M:%S UTC"
|
||||
)
|
||||
|
||||
|
||||
def find_newer_docs(project_root: Path, index_mtime: float) -> list[dict[str, Any]]:
|
||||
"""Return docs/*.md files whose mtime is newer than the sidecar index.md.
|
||||
|
||||
These are the most likely candidates for sidecar narrative integration —
|
||||
a freshly unpacked WIP update, session-context edit, or songbook
|
||||
addition that hasn't yet shown up in the sidecar's story.
|
||||
"""
|
||||
docs_root = project_root / "docs"
|
||||
if not docs_root.is_dir():
|
||||
return []
|
||||
|
||||
candidates: list[dict[str, Any]] = []
|
||||
for path in sorted(docs_root.rglob("*.md")):
|
||||
try:
|
||||
mtime = path.stat().st_mtime
|
||||
except OSError:
|
||||
continue
|
||||
if mtime <= index_mtime:
|
||||
continue
|
||||
rel = str(path.relative_to(project_root))
|
||||
candidates.append(
|
||||
{
|
||||
"path": rel,
|
||||
"mtime": _format_mtime(mtime),
|
||||
"delta_seconds": int(mtime - index_mtime),
|
||||
}
|
||||
)
|
||||
return candidates
|
||||
|
||||
|
||||
def run_validator(project_root: Path) -> dict[str, Any]:
|
||||
"""Invoke validate-sidecar.py and return its JSON payload.
|
||||
|
||||
Soft-fail if the validator isn't present — older installs or partial
|
||||
checkouts shouldn't break the reconcile flow.
|
||||
"""
|
||||
validator = Path(__file__).parent / "validate-sidecar.py"
|
||||
if not validator.is_file():
|
||||
return {"status": "skipped", "reason": "validate-sidecar.py not found"}
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(validator),
|
||||
str(project_root),
|
||||
"--format",
|
||||
"json",
|
||||
"--warn-only",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
except OSError as exc:
|
||||
return {"status": "error", "reason": f"could not invoke validator: {exc}"}
|
||||
|
||||
if result.returncode not in (0, 1):
|
||||
return {
|
||||
"status": "error",
|
||||
"reason": f"validator exited {result.returncode}",
|
||||
"stderr": result.stderr.strip(),
|
||||
}
|
||||
|
||||
try:
|
||||
return json.loads(result.stdout)
|
||||
except json.JSONDecodeError as exc:
|
||||
return {"status": "error", "reason": f"validator output unparseable: {exc}"}
|
||||
|
||||
|
||||
def format_text(payload: dict[str, Any]) -> str:
|
||||
lines = [
|
||||
"Sidecar Reconciliation Report",
|
||||
"=" * 29,
|
||||
"",
|
||||
]
|
||||
|
||||
status = payload.get("status", "unknown")
|
||||
lines.append(f"Status: {status}")
|
||||
lines.append(f"Sidecar index.md: {payload.get('index_path', 'unknown')}")
|
||||
if payload.get("index_mtime"):
|
||||
lines.append(f"Index last updated: {payload['index_mtime']}")
|
||||
lines.append("")
|
||||
|
||||
candidates = payload.get("newer_files", [])
|
||||
lines.append(
|
||||
f"Files modified more recently than the sidecar: {len(candidates)}"
|
||||
)
|
||||
if candidates:
|
||||
lines.append("")
|
||||
lines.append(
|
||||
"These are candidates for narrative integration. Review each and "
|
||||
"decide whether the sidecar's session history, current work, or "
|
||||
"catalog status should be updated before continuing:"
|
||||
)
|
||||
lines.append("")
|
||||
for item in candidates:
|
||||
lines.append(f" - {item['path']} (modified {item['mtime']})")
|
||||
lines.append("")
|
||||
|
||||
validator = payload.get("validator", {})
|
||||
v_status = validator.get("status", "unknown")
|
||||
lines.append(f"Validator: {v_status}")
|
||||
findings = validator.get("findings", []) or []
|
||||
if findings:
|
||||
by_category: dict[str, list[dict[str, Any]]] = {}
|
||||
for f in findings:
|
||||
by_category.setdefault(f.get("category", "other"), []).append(f)
|
||||
for category, items in sorted(by_category.items()):
|
||||
lines.append(f" [{category.upper()}] ({len(items)})")
|
||||
for f in items:
|
||||
lines.append(
|
||||
f" ({f.get('severity', 'warning')}) "
|
||||
f"{f.get('path', '')} — {f.get('message', '')}"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
if payload.get("needs_reconciliation"):
|
||||
lines.append(
|
||||
"ACTION NEEDED: walk the punch list above with the user and "
|
||||
"integrate changes into the sidecar narrative before packing "
|
||||
"a return sync."
|
||||
)
|
||||
else:
|
||||
lines.append("CLEAN: sidecar is in sync with unpacked file state.")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def build_report(project_root: Path) -> dict[str, Any]:
|
||||
index_path = (
|
||||
project_root / "_bmad" / "_memory" / "band-manager-sidecar" / "index.md"
|
||||
)
|
||||
payload: dict[str, Any] = {
|
||||
"index_path": str(
|
||||
index_path.relative_to(project_root)
|
||||
if index_path.is_relative_to(project_root)
|
||||
else index_path
|
||||
),
|
||||
}
|
||||
|
||||
if not index_path.is_file():
|
||||
payload["status"] = "no_sidecar"
|
||||
payload["newer_files"] = []
|
||||
payload["validator"] = {"status": "skipped", "reason": "no sidecar index.md"}
|
||||
payload["needs_reconciliation"] = False
|
||||
return payload
|
||||
|
||||
index_mtime = index_path.stat().st_mtime
|
||||
payload["index_mtime"] = _format_mtime(index_mtime)
|
||||
payload["newer_files"] = find_newer_docs(project_root, index_mtime)
|
||||
payload["validator"] = run_validator(project_root)
|
||||
|
||||
validator_findings = payload["validator"].get("findings", []) or []
|
||||
has_errors = any(f.get("severity") == "error" for f in validator_findings)
|
||||
payload["needs_reconciliation"] = bool(payload["newer_files"]) or has_errors
|
||||
payload["status"] = (
|
||||
"needs_reconciliation" if payload["needs_reconciliation"] else "clean"
|
||||
)
|
||||
return payload
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Post-unpack reconciliation helper for the Mac sidecar."
|
||||
)
|
||||
parser.add_argument(
|
||||
"project_root",
|
||||
nargs="?",
|
||||
default=".",
|
||||
help="Project root directory (default: current directory)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--format",
|
||||
choices=["text", "json"],
|
||||
default="text",
|
||||
help="Output format (default: text)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
project_root = Path(args.project_root).resolve()
|
||||
if not project_root.is_dir():
|
||||
print(f"ERROR: project root not found: {project_root}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
payload = build_report(project_root)
|
||||
|
||||
if args.format == "json":
|
||||
print(json.dumps(payload, indent=2))
|
||||
else:
|
||||
print(format_text(payload))
|
||||
|
||||
return 1 if payload.get("needs_reconciliation") else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user