feat(reconciliation): implement deterministic state reconciliation engine and post-commit hook
- Implement ReconciliationEngine in server/src/handlers/reconciliation.rs - Auto-transition ADRs to implemented, resolve tech debts, and cascade task unblocking - Ingest git commit events via POST /api/git/commit and scripts/git-reconcile.py post-commit hook - Add universal pagination, search filtering, and keyboard navigation to dashboard - Implement non-destructive task TTL expiry sweeper and gate verification - Implements: ADR-0102, ADR-0103
This commit is contained in:
1 parent
ec977c24dd
commit
8f924b793a
16 files changed
+2622
-163
No files matched your search
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Deterministic Git Reconciliation Hook for MCP Memory
|
||||
Automatically syncs git commit events to mcp-memory to transition ADRs, resolve tech debts,
|
||||
complete tasks, cascade-unblock dependent tasks, and recalculate milestone statuses.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
def run_git(cmd):
|
||||
try:
|
||||
res = subprocess.run(["git"] + cmd, capture_output=True, text=True, check=True)
|
||||
return res.stdout.strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def main():
|
||||
port = os.environ.get("MCP_PORT", "3000")
|
||||
|
||||
# 1. Quick check if server is running
|
||||
ping_url = f"http://127.0.0.1:{port}/ping"
|
||||
try:
|
||||
with urllib.request.urlopen(ping_url, timeout=1.0) as resp:
|
||||
if resp.status != 200:
|
||||
sys.exit(0)
|
||||
except Exception:
|
||||
# Server not running, silently exit
|
||||
sys.exit(0)
|
||||
|
||||
# 2. Extract git metadata
|
||||
commit = run_git(["rev-parse", "HEAD"])
|
||||
branch = run_git(["branch", "--show-current"])
|
||||
author = run_git(["log", "-1", "--pretty=format:%an"])
|
||||
message = run_git(["log", "-1", "--pretty=format:%B"])
|
||||
|
||||
# Get changed files in this commit
|
||||
files_raw = run_git(["diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD"])
|
||||
files = [f.strip() for f in files_raw.splitlines() if f.strip()]
|
||||
|
||||
if not commit or not message:
|
||||
sys.exit(0)
|
||||
|
||||
payload = {
|
||||
"commit": commit,
|
||||
"branch": branch,
|
||||
"author": author,
|
||||
"message": message,
|
||||
"files": files,
|
||||
}
|
||||
|
||||
# 3. Post to reconciliation endpoint
|
||||
reconcile_url = f"http://127.0.0.1:{port}/api/git/commit"
|
||||
req_data = json.dumps(payload).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
reconcile_url,
|
||||
data=req_data,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST"
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=3.0) as resp:
|
||||
if resp.status == 200:
|
||||
body = json.loads(resp.read().decode("utf-8"))
|
||||
res = body.get("reconciliation", {})
|
||||
adrs = res.get("implemented_adrs", [])
|
||||
debts = res.get("resolved_tech_debts", [])
|
||||
tasks = res.get("completed_tasks", [])
|
||||
unblocked = res.get("unblocked_tasks", [])
|
||||
milestones = res.get("updated_milestones", [])
|
||||
|
||||
if any([adrs, debts, tasks, unblocked, milestones]):
|
||||
print("\033[36m[MCP Memory Auto-Reconciled]\033[0m")
|
||||
if adrs:
|
||||
print(f" \033[32m✔ Implemented ADRs:\033[0m {', '.join(adrs)}")
|
||||
if debts:
|
||||
print(f" \033[32m✔ Resolved Tech Debts:\033[0m {', '.join(debts)}")
|
||||
if tasks:
|
||||
print(f" \033[32m✔ Completed Tasks:\033[0m {', '.join(tasks)}")
|
||||
if unblocked:
|
||||
print(f" \033[33m⚡ Unblocked Dependent Tasks:\033[0m {', '.join(unblocked)}")
|
||||
if milestones:
|
||||
print(f" \033[35m🎯 Updated Milestones:\033[0m {', '.join(milestones)}")
|
||||
except Exception:
|
||||
# Never break commit flow on failure
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user