Phase 1 Workstream A+E: thesis substrate + dual-approval gate

- migration 0002_phase1_architect: thesis_lines (core spine + per-segment lines),
  thesis_nodes (+ append-only revisions), thesis_versions (one-canonical-per-line
  DB invariant), thesis_reviews (dual approval + feedback), segments. Reversible.
- backend/mcp/architect_tools.py: agent draft tools (node tree, versions,
  segments, get_canonical fails-closed) — NO self-approval path. MCP-exposed.
- backend/thesis_review.py + server.py routes: human-gated approval. Dual sign-off
  via thesis_required_approvals; atomic supersede; every action logged.
- docs/PHASE_1.md (kickoff brief); docs/OPERATIONS.md (partner guide);
  start9/0.4 "Resolve duplicate names" fuzzy action.

Verified on synthetic data: dual approval promotes correctly, exactly one
canonical survives supersede, get_canonical fails closed, full interaction_log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Keysat
2026-06-05 10:20:00 -05:00
parent 6be2e40f54
commit 3e199fd8d5
10 changed files with 993 additions and 0 deletions
+69
View File
@@ -18,6 +18,7 @@ import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import crm_tools as t # noqa: E402
import architect_tools as at # noqa: E402
from mcp.server.fastmcp import FastMCP # noqa: E402
@@ -84,5 +85,73 @@ def set_entity_enrichment(lp_id: str, fields: dict, actor_id: str = "analyst") -
return t.set_entity_enrichment(lp_id, fields, actor_id=actor_id)
# ── Architect thesis tools (Phase 1; drafts only — no approve/promote here) ──
@mcp.tool()
def get_thesis(line_key: str) -> dict:
"""Fetch a thesis line and its node tree (throughline → sections → claims → proof-points)."""
return at.get_thesis(line_key)
@mcp.tool()
def list_thesis_lines() -> dict:
"""List all thesis lines (the core spine + per-segment lines)."""
return at.list_thesis_lines()
@mcp.tool()
def get_canonical_thesis(line_key: str) -> dict:
"""The current partner-APPROVED canonical thesis for a line. Fails closed if none approved."""
return at.get_canonical_thesis(line_key)
@mcp.tool()
def get_review_feedback(version_id: str) -> dict:
"""Partners' reviews/feedback on a thesis version — what to iterate on."""
return at.get_review_feedback(version_id)
@mcp.tool()
def create_thesis_line(line_key: str, name: str, segment_key: str = "", is_core: bool = False,
description: str = "") -> dict:
"""Create a new thesis line (a narrative, e.g. the core spine or a per-segment line)."""
return at.create_thesis_line(line_key, name, segment_key=segment_key or None,
is_core=is_core, description=description or None)
@mcp.tool()
def upsert_thesis_node(line_id: str, node_type: str, body: str, title: str = "", parent_id: str = "",
node_id: str = "", variant_group: str = "", change_reason: str = "") -> dict:
"""Create or edit a thesis node (a claim, section, proof-point, etc.). Edits are revisioned."""
return at.upsert_thesis_node(line_id, node_type, body, title=title or None,
parent_id=parent_id or None, node_id=node_id or None,
variant_group=variant_group or None, change_reason=change_reason or None)
@mcp.tool()
def create_thesis_version(line_key: str, rationale: str = "") -> dict:
"""Freeze the current node tree into an immutable DRAFT version (stays draft until a human approves)."""
return at.create_thesis_version(line_key, rationale=rationale or None)
@mcp.tool()
def submit_version_for_review(version_id: str) -> dict:
"""Move a draft thesis version to 'in_review' so the partners can weigh in. Cannot make it canonical."""
return at.submit_version_for_review(version_id)
@mcp.tool()
def list_segments() -> dict:
"""List active LP segment definitions."""
return at.list_segments()
@mcp.tool()
def upsert_segment(segment_key: str, name: str, definition: str = "", needs_to_hear: str = "",
avoid: str = "") -> dict:
"""Create/replace an LP segment's active definition."""
return at.upsert_segment(segment_key, name, definition=definition or None,
needs_to_hear=needs_to_hear or None, avoid=avoid or None)
if __name__ == "__main__":
mcp.run()