-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
347 lines (301 loc) · 28.6 KB
/
Copy pathapp.py
File metadata and controls
347 lines (301 loc) · 28.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
#!/usr/bin/env python3
"""Dependency-free Agentic Shared Project Memory web server and JSON API."""
from __future__ import annotations
import hashlib
import json
import os
import re
import sqlite3
import argparse
from datetime import UTC, datetime, timedelta
from http import HTTPStatus
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import parse_qs, urlparse
import agent_runtime
from runtime_adapters import AdapterError, GitHubSourceAdapter, Settings, StructuredModelAdapter
ROOT = Path(__file__).parent
DB_PATH = ROOT / "project-memory.db"
STATIC = ROOT / "static"
SECRET_RE = re.compile(r"(?i)(gh[pousr]_[a-z0-9_]{20,}|(?:api[_-]?key|secret|token)\s*[=:]\s*[^\s]{12,})")
ACTORS = {"maya": "engineer", "sam": "engineer", "robin": "lead"}
def now() -> str:
return datetime.now(UTC).isoformat(timespec="seconds")
def mask_sensitive(text: str) -> str:
return SECRET_RE.sub("[REDACTED: suspected secret]", text)
def digest(text: str) -> str:
return hashlib.sha256(text.encode()).hexdigest()
def db() -> sqlite3.Connection:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON")
return conn
SCHEMA = """
CREATE TABLE IF NOT EXISTS source_items (id INTEGER PRIMARY KEY, kind TEXT NOT NULL, external_id TEXT NOT NULL UNIQUE, title TEXT NOT NULL, source_url TEXT NOT NULL, author TEXT, occurred_at TEXT NOT NULL, content_hash TEXT NOT NULL, excerpt TEXT NOT NULL, paths TEXT NOT NULL DEFAULT '[]', metadata TEXT NOT NULL DEFAULT '{}');
CREATE TABLE IF NOT EXISTS handoffs (id INTEGER PRIMARY KEY, author TEXT NOT NULL, window_start TEXT NOT NULL, window_end TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'draft', attested_at TEXT, submitted_at TEXT, draft_summary TEXT NOT NULL DEFAULT '');
CREATE TABLE IF NOT EXISTS memory_items (id INTEGER PRIMARY KEY, handoff_id INTEGER REFERENCES handoffs(id), kind TEXT NOT NULL, state TEXT NOT NULL DEFAULT 'active', body TEXT NOT NULL, author TEXT NOT NULL, approved_at TEXT, approved_by TEXT, paths TEXT NOT NULL DEFAULT '[]', component TEXT, ticket_ref TEXT, created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS evidence_links (item_id INTEGER REFERENCES memory_items(id), source_id INTEGER REFERENCES source_items(id), role TEXT NOT NULL, PRIMARY KEY(item_id, source_id, role));
CREATE TABLE IF NOT EXISTS knowledge_relations (id INTEGER PRIMARY KEY, from_item INTEGER REFERENCES memory_items(id), to_item INTEGER REFERENCES memory_items(id), relation_type TEXT NOT NULL, proposed INTEGER NOT NULL DEFAULT 1);
CREATE TABLE IF NOT EXISTS review_findings (id INTEGER PRIMARY KEY, handoff_id INTEGER REFERENCES handoffs(id), severity TEXT NOT NULL, category TEXT NOT NULL, body TEXT NOT NULL, requires_response INTEGER NOT NULL, response TEXT, disposition TEXT NOT NULL DEFAULT 'open');
CREATE TABLE IF NOT EXISTS manual_notes (id INTEGER PRIMARY KEY, handoff_id INTEGER REFERENCES handoffs(id), note_type TEXT NOT NULL, body TEXT NOT NULL, attachment TEXT, sanitized INTEGER NOT NULL DEFAULT 1);
CREATE TABLE IF NOT EXISTS audit_events (id INTEGER PRIMARY KEY, actor TEXT NOT NULL, action TEXT NOT NULL, entity_type TEXT NOT NULL, entity_id INTEGER NOT NULL, payload TEXT NOT NULL DEFAULT '{}', occurred_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS agent_runs (id INTEGER PRIMARY KEY, handoff_id INTEGER NOT NULL REFERENCES handoffs(id), agent_name TEXT NOT NULL, contract_hash TEXT NOT NULL, input_hash TEXT NOT NULL, output_json TEXT, status TEXT NOT NULL, latency_ms INTEGER, error_message TEXT, created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS draft_promotions (agent_run_id INTEGER NOT NULL REFERENCES agent_runs(id), memory_item_id INTEGER NOT NULL REFERENCES memory_items(id), PRIMARY KEY(agent_run_id, memory_item_id));
CREATE TABLE IF NOT EXISTS review_promotions (agent_run_id INTEGER NOT NULL REFERENCES agent_runs(id), finding_id INTEGER NOT NULL REFERENCES review_findings(id), PRIMARY KEY(agent_run_id, finding_id));
CREATE TABLE IF NOT EXISTS review_evidence_links (finding_id INTEGER NOT NULL REFERENCES review_findings(id), source_id INTEGER NOT NULL REFERENCES source_items(id), PRIMARY KEY(finding_id, source_id));
"""
def audit(conn: sqlite3.Connection, actor: str, action: str, entity: str, entity_id: int, **payload: object) -> None:
conn.execute("INSERT INTO audit_events(actor,action,entity_type,entity_id,payload,occurred_at) VALUES(?,?,?,?,?,?)", (actor, action, entity, entity_id, json.dumps(payload), now()))
def init_db() -> None:
fresh = not DB_PATH.exists()
with db() as conn:
conn.executescript(SCHEMA)
if not fresh or conn.execute("SELECT count(*) FROM source_items").fetchone()[0]:
return
base = datetime.now(UTC) - timedelta(hours=8)
sources = [
("commit", "a13bf2", "Add request retry classification", "https://github.com/acme/payments/commit/a13bf2", "maya", base + timedelta(hours=1), "Retry policy now distinguishes retryable network errors from terminal upstream errors.", ["services/gateway/retry.py"]),
("pull_request", "42", "PR #42: Classify gateway retry failures", "https://github.com/acme/payments/pull/42", "maya", base + timedelta(hours=2), "Changes retry classification and adds focused unit coverage for network errors.", ["services/gateway/retry.py", "tests/test_retry.py"]),
("review", "42:robin", "Robin requested a backoff-cap test", "https://github.com/acme/payments/pull/42#pullrequestreview-1", "robin", base + timedelta(hours=3), "Please add a test proving retry attempts stop at the configured backoff cap.", ["tests/test_retry.py"]),
("check", "42:unit", "CI: unit tests incomplete", "https://github.com/acme/payments/actions/runs/1001", "github-actions", base + timedelta(hours=4), "Unit suite passed, but the integration retry scenario was skipped because its fixture is unavailable.", ["tests/test_retry.py"]),
("document", "docs:retries", "Architecture: gateway retry boundaries", "https://github.com/acme/payments/blob/main/docs/architecture/retries.md", "maya", base + timedelta(hours=5), "The gateway owns retry classification; callers receive a typed terminal failure after bounded attempts.", ["docs/architecture/retries.md"]),
]
for kind, ext, title, url, author, stamp, excerpt, paths in sources:
conn.execute("INSERT INTO source_items(kind,external_id,title,source_url,author,occurred_at,content_hash,excerpt,paths) VALUES(?,?,?,?,?,?,?,?,?)", (kind, ext, title, url, author, stamp.isoformat(timespec="seconds"), digest(excerpt), mask_sensitive(excerpt), json.dumps(paths)))
conn.execute("INSERT INTO handoffs(author,window_start,window_end,status,draft_summary) VALUES(?,?,?,?,?)", ("maya", base.isoformat(timespec="seconds"), now(), "draft", "Implemented retry classification and opened PR #42. The unit suite passes."))
handoff_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
items = [
("finding", "active", "Gateway retries now classify network failures separately from terminal upstream failures.", "maya", ["services/gateway/retry.py"], "Gateway", "PAY-184", 1),
("conclusion", "active", "Retry behavior is fully covered by tests.", "maya", ["tests/test_retry.py"], "Gateway", "PAY-184", 4),
("continuation", "active", "Add and verify the backoff-cap integration scenario before merging PR #42.", "maya", ["tests/test_retry.py"], "Gateway", "PAY-184", 3),
]
for kind, state, body, author, paths, component, ticket, source_id in items:
conn.execute("INSERT INTO memory_items(handoff_id,kind,state,body,author,paths,component,ticket_ref,created_at) VALUES(?,?,?,?,?,?,?,?,?)", (handoff_id, kind, state, body, author, json.dumps(paths), component, ticket, now()))
item_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
conn.execute("INSERT INTO evidence_links VALUES(?,?,?)", (item_id, source_id, "supports"))
conn.execute("INSERT INTO review_findings(handoff_id,severity,category,body,requires_response) VALUES(?,?,?,?,?)", (handoff_id, "high", "missing-test", "The draft says retry behavior is fully covered, but the linked CI check says the integration retry scenario was skipped. Clarify or defer this conclusion.", 1))
conn.execute("INSERT INTO review_findings(handoff_id,severity,category,body,requires_response) VALUES(?,?,?,?,?)", (handoff_id, "medium", "omitted-activity", "The reviewer requested a backoff-cap test on PR #42; it is not mentioned in the author summary.", 0))
audit(conn, "system", "seeded", "handoff", handoff_id, demo=True)
def row_dict(row: sqlite3.Row) -> dict:
out = dict(row)
for key in ("paths", "metadata", "payload"):
if key in out:
out[key] = json.loads(out[key] or ("[]" if key == "paths" else "{}"))
return out
def handoff_payload(conn: sqlite3.Connection, handoff_id: int) -> dict:
handoff = row_dict(conn.execute("SELECT * FROM handoffs WHERE id=?", (handoff_id,)).fetchone())
sources = [row_dict(r) for r in conn.execute("SELECT * FROM source_items ORDER BY occurred_at DESC")]
items = [row_dict(r) for r in conn.execute("SELECT * FROM memory_items WHERE handoff_id=? ORDER BY id", (handoff_id,))]
for item in items:
item["evidence"] = [row_dict(r) for r in conn.execute("SELECT s.* FROM source_items s JOIN evidence_links e ON e.source_id=s.id WHERE e.item_id=?", (item["id"],))]
findings = [row_dict(r) for r in conn.execute("SELECT * FROM review_findings WHERE handoff_id=? ORDER BY requires_response DESC, id", (handoff_id,))]
notes = [row_dict(r) for r in conn.execute("SELECT * FROM manual_notes WHERE handoff_id=?", (handoff_id,))]
return {"handoff": handoff, "sources": sources, "items": items, "findings": findings, "notes": notes}
def current_handoff(conn: sqlite3.Connection, author: str = "maya") -> int:
row = conn.execute("SELECT id FROM handoffs WHERE author=? ORDER BY id DESC LIMIT 1", (author,)).fetchone()
if row:
return row[0]
conn.execute("INSERT INTO handoffs(author,window_start,window_end,status,draft_summary) VALUES(?,?,?,?,?)", (author, now(), now(), "draft", ""))
return conn.execute("SELECT last_insert_rowid()").fetchone()[0]
def upsert_source(conn: sqlite3.Connection, source: dict) -> bool:
"""Preserve first-seen evidence; never silently overwrite its quoted excerpt."""
excerpt = mask_sensitive(source["excerpt"])
result = conn.execute("INSERT OR IGNORE INTO source_items(kind,external_id,title,source_url,author,occurred_at,content_hash,excerpt,paths,metadata) VALUES(?,?,?,?,?,?,?,?,?,?)", (source["kind"], source["external_id"], source["title"], source["source_url"], source.get("author"), source["occurred_at"], digest(excerpt), excerpt, json.dumps(source.get("paths", [])), json.dumps(source.get("metadata", {}))))
return result.rowcount == 1
def agent_input(conn: sqlite3.Connection, handoff_id: int, agent_name: str) -> dict:
handoff = row_dict(conn.execute("SELECT * FROM handoffs WHERE id=?", (handoff_id,)).fetchone())
sources = [row_dict(r) for r in conn.execute("SELECT * FROM source_items WHERE occurred_at >= ? AND occurred_at <= ? ORDER BY occurred_at", (handoff["window_start"], handoff["window_end"]))]
notes = [row_dict(r) for r in conn.execute("SELECT * FROM manual_notes WHERE handoff_id=?", (handoff_id,))]
payload = {"handoff": handoff, "sources": sources, "manual_notes": notes}
if agent_name == "evidence-reviewer":
payload["proposed_items"] = [row_dict(r) for r in conn.execute("SELECT * FROM memory_items WHERE handoff_id=? AND approved_at IS NULL", (handoff_id,))]
payload["approved_memory"] = [row_dict(r) for r in conn.execute("SELECT * FROM memory_items WHERE approved_at IS NOT NULL ORDER BY approved_at DESC LIMIT 50")]
synthesis = conn.execute("SELECT output_json FROM agent_runs WHERE handoff_id=? AND agent_name='activity-synthesizer' AND status='succeeded' ORDER BY id DESC LIMIT 1", (handoff_id,)).fetchone()
payload["synthesis_draft"] = json.loads(synthesis[0]) if synthesis else None
return payload
def execute_agent(conn: sqlite3.Connection, handoff_id: int, agent_name: str) -> dict:
payload = agent_input(conn, handoff_id, agent_name)
contract = agent_runtime.load_contract(agent_name)
created_at = now()
try:
output, latency = StructuredModelAdapter(Settings.from_env()).run(agent_name, payload)
conn.execute("INSERT INTO agent_runs(handoff_id,agent_name,contract_hash,input_hash,output_json,status,latency_ms,created_at) VALUES(?,?,?,?,?,?,?,?)", (handoff_id, agent_name, digest(contract), digest(json.dumps(payload, sort_keys=True)), json.dumps(output), "succeeded", latency, created_at))
run_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
return {"id": run_id, "status": "succeeded", "output": output, "latency_ms": latency}
except AdapterError as error:
conn.execute("INSERT INTO agent_runs(handoff_id,agent_name,contract_hash,input_hash,status,error_message,created_at) VALUES(?,?,?,?,?,?,?)", (handoff_id, agent_name, digest(contract), digest(json.dumps(payload, sort_keys=True)), "failed", str(error), created_at))
run_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
return {"id": run_id, "status": "failed", "error": str(error)}
def require_source_ids(conn: sqlite3.Connection, evidence_ids: object) -> list[int]:
if not isinstance(evidence_ids, list) or not evidence_ids:
raise ValueError("Every proposed item needs at least one numeric source_item evidence ID.")
ids = [value for value in evidence_ids if isinstance(value, int)]
if len(ids) != len(evidence_ids):
raise ValueError("Evidence IDs must be numeric source_item IDs from the agent input.")
found = {row[0] for row in conn.execute(f"SELECT id FROM source_items WHERE id IN ({','.join('?' for _ in ids)})", ids)}
if found != set(ids):
raise ValueError("A proposed item referenced source evidence outside this project.")
return ids
def promote_synthesis(conn: sqlite3.Connection, handoff_id: int, run_id: int, actor: str) -> int:
run = conn.execute("SELECT * FROM agent_runs WHERE id=? AND handoff_id=? AND agent_name='activity-synthesizer' AND status='succeeded'", (run_id, handoff_id)).fetchone()
if not run:
raise ValueError("A successful synthesis run for this handoff is required.")
if conn.execute("SELECT 1 FROM draft_promotions WHERE agent_run_id=?", (run_id,)).fetchone():
raise ValueError("This synthesis run was already adopted as a draft.")
output = json.loads(run["output_json"])
allowed_kinds = {"finding", "fact", "assumption", "hypothesis", "conclusion", "decision", "blocker", "continuation"}
created = 0
for proposal in output["proposed_items"]:
kind, body = proposal.get("kind"), proposal.get("body", "").strip()
if kind not in allowed_kinds or not body:
raise ValueError("Every proposed item needs an allowed kind and non-empty body.")
evidence_ids = require_source_ids(conn, proposal.get("evidence_ids"))
conn.execute("INSERT INTO memory_items(handoff_id,kind,state,body,author,paths,component,ticket_ref,created_at) VALUES(?,?,?,?,?,?,?,?,?)", (handoff_id, kind, "active", body, actor, json.dumps(proposal.get("path_scopes", [])), proposal.get("component_suggestion"), None, now()))
item_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
for source_id in evidence_ids:
conn.execute("INSERT INTO evidence_links VALUES(?,?,?)", (item_id, source_id, "supports"))
conn.execute("INSERT INTO draft_promotions VALUES(?,?)", (run_id, item_id))
created += 1
return created
def promote_review(conn: sqlite3.Connection, handoff_id: int, run_id: int) -> int:
run = conn.execute("SELECT * FROM agent_runs WHERE id=? AND handoff_id=? AND agent_name='evidence-reviewer' AND status='succeeded'", (run_id, handoff_id)).fetchone()
if not run:
raise ValueError("A successful evidence-review run for this handoff is required.")
if conn.execute("SELECT 1 FROM review_promotions WHERE agent_run_id=?", (run_id,)).fetchone():
raise ValueError("This review run was already applied.")
created = 0
for finding in json.loads(run["output_json"])["findings"]:
severity = finding.get("severity")
if severity not in {"high", "medium", "low"} or not finding.get("body", "").strip():
raise ValueError("Every review finding needs a known severity and non-empty body.")
evidence_ids = require_source_ids(conn, finding.get("evidence_ids"))
body = finding["body"].strip()
if finding.get("question"):
body += f"\n\nQuestion: {finding['question'].strip()}"
conn.execute("INSERT INTO review_findings(handoff_id,severity,category,body,requires_response) VALUES(?,?,?,?,?)", (handoff_id, severity, finding.get("category", "evidence-gap"), body, int(bool(finding.get("requires_response")))))
finding_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
for source_id in evidence_ids:
conn.execute("INSERT INTO review_evidence_links VALUES(?,?)", (finding_id, source_id))
conn.execute("INSERT INTO review_promotions VALUES(?,?)", (run_id, finding_id))
created += 1
return created
def mcp_context(conn: sqlite3.Connection) -> dict:
approved = [row_dict(r) for r in conn.execute("SELECT * FROM memory_items WHERE approved_at IS NOT NULL AND state IN ('active','disputed') ORDER BY approved_at DESC")]
pending = [row_dict(r) for r in conn.execute("SELECT * FROM memory_items WHERE approved_at IS NULL ORDER BY created_at DESC")]
return {"canonical": approved, "pending_handoff_items": pending, "notice": "Pending items are author-submitted or draft evidence, not canonical project memory."}
class Handler(SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=str(STATIC), **kwargs)
def send_json(self, payload: object, status: int = 200) -> None:
raw = json.dumps(payload, default=str).encode()
self.send_response(status); self.send_header("Content-Type", "application/json"); self.send_header("Content-Length", str(len(raw))); self.end_headers(); self.wfile.write(raw)
def actor(self) -> str:
return self.headers.get("X-Project-Memory-Actor", "maya")
def read_json(self) -> dict:
size = int(self.headers.get("Content-Length", "0")); return json.loads(self.rfile.read(size) or b"{}")
def do_GET(self) -> None:
parsed = urlparse(self.path)
if parsed.path == "/api/handoff":
with db() as conn: self.send_json(handoff_payload(conn, current_handoff(conn, self.actor()))); return
if parsed.path == "/api/status":
with db() as conn:
approved = [row_dict(r) for r in conn.execute("SELECT * FROM memory_items WHERE approved_at IS NOT NULL ORDER BY approved_at DESC")]
self.send_json({"recent": approved[:5], "blockers": [x for x in approved if x["kind"] == "blocker" and x["state"] == "active"], "needs_attention": [x for x in approved if x["kind"] == "decision" and x["state"] == "disputed"]}); return
if parsed.path == "/api/audit":
with db() as conn: self.send_json([row_dict(r) for r in conn.execute("SELECT * FROM audit_events ORDER BY id DESC LIMIT 20")]); return
if parsed.path == "/api/agent-runs":
with db() as conn:
handoff_id = int(parse_qs(parsed.query).get("handoff_id", [str(current_handoff(conn, self.actor()))])[0])
runs = [row_dict(r) for r in conn.execute("SELECT * FROM agent_runs WHERE handoff_id=? ORDER BY id DESC", (handoff_id,))]
for run in runs:
if run.get("output_json"):
run["output"] = json.loads(run.pop("output_json"))
self.send_json(runs); return
if parsed.path.startswith("/api/mcp/"):
query = parse_qs(parsed.query)
with db() as conn:
if parsed.path.endswith("handoff-context"): self.send_json(mcp_context(conn)); return
if parsed.path.endswith("search"):
term = query.get("query", [""])[0].lower(); rows = [row_dict(r) for r in conn.execute("SELECT * FROM memory_items WHERE approved_at IS NOT NULL") if term in r["body"].lower()]; self.send_json({"results": rows, "citations_required": True}); return
if parsed.path.endswith("item-evidence"):
item = int(query.get("id", ["0"])[0]); rows = [row_dict(r) for r in conn.execute("SELECT s.* FROM source_items s JOIN evidence_links e ON e.source_id=s.id WHERE e.item_id=?", (item,))]; self.send_json({"item_id": item, "evidence": rows}); return
if parsed.path.endswith("architecture-context"):
scope = query.get("scope", [""])[0].lower(); rows = [row_dict(r) for r in conn.execute("SELECT * FROM memory_items WHERE approved_at IS NOT NULL") if scope in (r["component"] or "").lower() or any(scope in p.lower() for p in json.loads(r["paths"]))]; self.send_json({"scope": scope, "items": rows}); return
if parsed.path == "/": self.path = "/index.html"
return super().do_GET()
def do_POST(self) -> None:
parsed = urlparse(self.path); actor = self.actor(); body = self.read_json()
if actor not in ACTORS: self.send_json({"error": "unknown actor"}, 401); return
with db() as conn:
hid = current_handoff(conn, actor)
if parsed.path == "/api/source-refresh":
try:
github_login = body.get("github_login", actor)
conn.execute("UPDATE handoffs SET window_end=? WHERE id=?", (now(), hid))
sources = GitHubSourceAdapter(Settings.from_env()).fetch_since(github_login, conn.execute("SELECT window_start FROM handoffs WHERE id=?", (hid,)).fetchone()[0])
added = sum(upsert_source(conn, source) for source in sources)
audit(conn, actor, "refreshed_github_sources", "handoff", hid, fetched=len(sources), added=added)
self.send_json({"handoff_id": hid, "fetched": len(sources), "added": added}); return
except AdapterError as error:
self.send_json({"error": str(error)}, 503); return
if parsed.path == "/api/agent-runs":
agent_name = body.get("agent_name")
if agent_name not in {"activity-synthesizer", "evidence-reviewer"}:
self.send_json({"error": "agent_name must be activity-synthesizer or evidence-reviewer."}, 400); return
result = execute_agent(conn, hid, agent_name)
audit(conn, actor, "ran_agent", "agent_run", result["id"], agent=agent_name, status=result["status"])
self.send_json(result, 201 if result["status"] == "succeeded" else 503); return
if parsed.path == "/api/promote-synthesis":
try:
created = promote_synthesis(conn, hid, int(body["agent_run_id"]), actor)
audit(conn, actor, "adopted_synthesis_draft", "handoff", hid, created_items=created, agent_run_id=body["agent_run_id"])
self.send_json({"created_draft_items": created}, 201); return
except (KeyError, TypeError, ValueError) as error:
self.send_json({"error": str(error)}, 409); return
if parsed.path == "/api/promote-review":
try:
created = promote_review(conn, hid, int(body["agent_run_id"]))
audit(conn, actor, "applied_evidence_review", "handoff", hid, created_findings=created, agent_run_id=body["agent_run_id"])
self.send_json({"created_review_findings": created}, 201); return
except (KeyError, TypeError, ValueError) as error:
self.send_json({"error": str(error)}, 409); return
if parsed.path == "/api/notes":
text = mask_sensitive(body.get("body", "").strip())
if not text: self.send_json({"error": "note body required"}, 400); return
conn.execute("INSERT INTO manual_notes(handoff_id,note_type,body,attachment,sanitized) VALUES(?,?,?,?,?)", (hid, body.get("note_type", "observation"), text, mask_sensitive(body.get("attachment", "")), 1)); audit(conn, actor, "added_manual_note", "handoff", hid); self.send_json(handoff_payload(conn, hid), 201); return
if parsed.path == "/api/draft-summary":
summary = body.get("summary", "").strip()
if not summary: self.send_json({"error": "A handoff summary is required."}, 400); return
conn.execute("UPDATE handoffs SET draft_summary=? WHERE id=?", (summary, hid)); audit(conn, actor, "updated_draft_summary", "handoff", hid); self.send_json(handoff_payload(conn, hid)); return
if parsed.path == "/api/review-response":
finding_id = int(body["finding_id"]); response = body.get("response", "").strip(); conn.execute("UPDATE review_findings SET response=?, disposition=? WHERE id=? AND handoff_id=?", (response, "deferred" if body.get("defer") else "addressed", finding_id, hid)); audit(conn, actor, "responded_to_review", "review_finding", finding_id); self.send_json(handoff_payload(conn, hid)); return
if parsed.path == "/api/submit":
open_high = conn.execute("SELECT count(*) FROM review_findings WHERE handoff_id=? AND requires_response=1 AND (response IS NULL OR response='')", (hid,)).fetchone()[0]
if open_high: self.send_json({"error": "High-risk reviewer findings need a response or deferral."}, 409); return
conn.execute("UPDATE handoffs SET status='submitted', attested_at=?, submitted_at=? WHERE id=?", (now(), now(), hid)); audit(conn, actor, "submitted_and_attested", "handoff", hid); self.send_json(handoff_payload(conn, hid)); return
if parsed.path == "/api/approve":
if ACTORS[actor] != "lead": self.send_json({"error": "Only the lead can approve canonical memory."}, 403); return
item_id = int(body["item_id"]); conn.execute("UPDATE memory_items SET approved_at=?, approved_by=? WHERE id=?", (now(), actor, item_id)); audit(conn, actor, "approved", "memory_item", item_id); self.send_json({"ok": True}); return
if parsed.path == "/api/reject":
if ACTORS[actor] != "lead": self.send_json({"error": "Only the lead can reject canonical memory."}, 403); return
item_id = int(body["item_id"]); conn.execute("UPDATE memory_items SET state='rejected' WHERE id=?", (item_id,)); audit(conn, actor, "rejected", "memory_item", item_id); self.send_json({"ok": True}); return
if parsed.path == "/api/correct":
if ACTORS[actor] != "lead": self.send_json({"error": "Only the lead can correct canonical memory."}, 403); return
original_id = int(body["item_id"]); replacement = body.get("body", "").strip()
original = conn.execute("SELECT * FROM memory_items WHERE id=?", (original_id,)).fetchone()
if not original or not replacement: self.send_json({"error": "Existing item and replacement text are required."}, 400); return
conn.execute("INSERT INTO memory_items(handoff_id,kind,state,body,author,approved_at,approved_by,paths,component,ticket_ref,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)", (original["handoff_id"], body.get("kind", original["kind"]), "active", replacement, actor, now(), actor, original["paths"], original["component"], original["ticket_ref"], now()))
new_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
conn.execute("INSERT INTO evidence_links SELECT ?, source_id, role FROM evidence_links WHERE item_id=?", (new_id, original_id))
conn.execute("UPDATE memory_items SET state='superseded' WHERE id=?", (original_id,))
conn.execute("INSERT INTO knowledge_relations(from_item,to_item,relation_type,proposed) VALUES(?,?,?,0)", (new_id, original_id, "supersedes"))
audit(conn, actor, "appended_correction", "memory_item", new_id, supersedes=original_id); self.send_json({"ok": True, "replacement_id": new_id}); return
self.send_json({"error": "unknown endpoint"}, HTTPStatus.NOT_FOUND)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run the Agentic Shared Project Memory web application.")
parser.add_argument("--host", default=os.getenv("PROJECT_MEMORY_HOST", "127.0.0.1"))
parser.add_argument("--port", type=int, default=int(os.getenv("PROJECT_MEMORY_PORT", "8080")))
args = parser.parse_args()
init_db()
print(f"Agentic Shared Project Memory running at http://{args.host}:{args.port}")
ThreadingHTTPServer((args.host, args.port), Handler).serve_forever()