Every ticket as the root of its provenance graph — goal, patch, approval, runs and (as the ticket-class build lands) mandate, manifestations, guarantees, journey, artifacts and children. Rendered through the <<<TicketGraphList>>> tag.
goal
THIS UNIT SUPERSEDES #288 — it is the SAME module and the same work, re-filed to be
SELF-CONTAINED. The superseded unit named its source only as a `git cat-file` command
against the forge's bare repos, which the sandboxed pair_loop worker that executes these
tickets cannot run, so it could not be done as filed (#346; #259 is the proof). A ticket body
is permanent in SQL (tg_tickets_immutable_identity, migrations/0029_ticket_authorization
.sql:111-117), so that body could not be repaired — this ticket carries the fix instead, and
the original is halted by a stop ticket rather than erased. Do the work HERE.
Rewrite ONE unique forge module as entirely new, behavior-preserving, installable,
control-config'd Python, per the FORGE-wide refactor standard #200 (#std).
This ticket covers ONE module — the one identified below — and nothing else. It was filed
against the module's DEDUPLICATED identity, so if that module was copy-pasted across the
forge, every copy is covered by this single ticket.
MODULE KEY : forge-rw:b94f61804a14beb2
(the ticketer's idempotency marker — it lives in this ticket's TITLE. Do
not edit the title, or the module will be re-filed as a duplicate.)
IDENTITY : AST-NORMALIZED — codebean's `_ast_norm` (#183), i.e.
ast.dump(ast.parse(ast.unparse(ast.parse(src)))).
Copies of this module that differ only in comments, blank lines,
quoting or spacing carry the SAME module-sha and are covered by THIS
ticket. A differing docstring is real content, so a module whose
docstring differs is a DIFFERENT module with its own ticket.
SIZE : 13835 bytes
EXTENSION : .py
CANONICAL SOURCE (the location this ticket is named for):
repo : code__code-invalid__coupler
path : coupler/server.py
ref : refs/heads/stable (as recorded when this module was first filed)
blob : 257bf1163cd84c3cdd1fe82da9c7ce2c8b323e0a (git blob sha1)
REFERENCE ONLY — you do NOT need this command. This module's bytes are embedded
COMPLETE in THE ORIGINAL SOURCE block below, and that is what the rewrite is based
on. For a reader who does have forge access:
git --git-dir=/Users/stevenpeterson/code/installed/jazz-project/config/forge/remotes/code__code-invalid__coupler.git cat-file blob 257bf1163cd84c3cdd1fe82da9c7ce2c8b323e0a
THE SAME MODULE ALSO LIVES AT THESE FORGE TIPS: none — this module is unique to
code__code-invalid__coupler among the repos scanned so far.
HISTORICAL OCCURRENCES (omni-git `occ`): none to add — either the index has no
row for this blob (it is newer than the index's last snapshot, or the index does
not cover its repo), or every place it records is already listed live above.
THE ORIGINAL SOURCE, EMBEDDED — YOU NEED NO FORGE ACCESS TO DO THIS TICKET.
The module's own bytes are reproduced below, inside this ticket. THEY are the
authoritative copy and the behavior-preserving basis: rewrite from them. The
`git --git-dir=... cat-file blob` command under CANONICAL SOURCE above is kept only as a
REFERENCE for a reader who happens to have forge access — running it is NOT part of this
ticket. (Why the bytes are here at all: the pair_loop worker that executes this ticket is
sandboxed to the ticket's own congruency worktree and cannot read the forge's bare repos.
Ticket #346 is the fix that put them on the ticket.)
The block BEGINS at the line
===ORIGINAL SOURCE (verbatim, the behavior-preserving basis)===
and ENDS at the first line
===END SOURCE===
Everything strictly between those two lines is the module — 13835 byte(s), byte for byte, with no re-indentation, no re-wrapping and nothing
elided.
===ORIGINAL SOURCE (verbatim, the behavior-preserving basis)===
#!/usr/bin/env python3
"""
coupler/server.py — HTTP MCP server on localhost:7770.
Run this as a long-lived daemon. Restart it to deploy updates.
Claude Desktop never needs to restart — bridge.py reconnects automatically.
Usage:
python -m coupler.server [--port 7770]
"""
import argparse
import json
import os
import sys
from pathlib import Path
import httpx
from fastmcp import FastMCP, Client
from fastmcp.client.transports import StdioTransport
from fastmcp.server import create_proxy
# ── jazz_telemetry (graceful: never hard-fail if the lib isn't present) ─────────
sys.path.insert(0, str(Path.home() / "code" / "forge" / "work" / "jazz_telemetry"))
try:
from jazz_telemetry import telemetry, service_running
except Exception: # pragma: no cover - telemetry is best-effort
from contextlib import contextmanager as _cm
def telemetry(*_a, **_k):
class _N:
def emit(self, *a, **k):
pass
return _N()
@_cm
def service_running(*_a, **_k):
yield None
JAZZ_ROOT = Path(os.environ.get("JAZZ_ROOT", Path.home() / "code/workspace/main"))
JAZZ_DEV = Path(os.environ.get("JAZZ_DEV", Path.home() / "code/jazz"))
def _env_port(name: str, default: int) -> int:
"""Read an integer port from env, tolerating Kubernetes service-link vars.
k8s injects <SERVICENAME>_PORT=tcp://<clusterip>:<port> for every Service in
the namespace. When the Service is named 'coupler', that clobbers our own
COUPLER_PORT with a 'tcp://…' URL. Fall back to the default (or the URL's
trailing port) instead of crashing on int('tcp://…')."""
raw = os.environ.get(name)
if raw is None:
return default
try:
return int(raw)
except ValueError:
tail = raw.rsplit(":", 1)[-1] # 'tcp://10.43.0.1:8000' -> '8000'
try:
return int(tail)
except ValueError:
return default
PORT = _env_port("COUPLER_PORT", 7770)
def _load_build() -> dict:
p = JAZZ_ROOT / "conf/env/build.json"
return json.loads(p.read_text()) if p.exists() else {}
# Downstream jazz MCP servers the coupler can mount — single source of truth.
# notes_mcp retired: a JSON tree-of-notes prototype over conf/notes/notes.json, NOT a
# notes_db client (a red herring). The real brain is notes_db (:8001) / notes_db_mcp (:8002).
JAZZ_SERVICES = [
("notes_db", "notes_db_mcp"),
("z3", "z3_mcp"),
("codebean", "codebean_mcp"),
("bootstrap", "bootstrap_mcp"),
("services", "service_registry_mcp"),
("volctl", "volctl"),
("gs", "git_surface_mcp"), # git surface + VM control (vm_* tools, machine time-travel)
]
def _disabled_services() -> set:
"""Which MCP servers to skip. Sources, merged:
• env COUPLER_DISABLE="z3,notes" (comma-separated)
• build.json {"coupler": {"disabled": ["z3"]}} or {"coupler": {"z3": false}}
Default: everything enabled."""
names = {s.strip() for s in os.environ.get("COUPLER_DISABLE", "").split(",") if s.strip()}
cfg = _load_build().get("coupler", {}) or {}
names |= set(cfg.get("disabled", []))
names |= {k for k, v in cfg.items() if k != "disabled" and v is False}
return names
def _notes_python() -> str:
"""Interpreter used to launch downstream MCP subprocesses.
They run under the SAME venv as coupler — whatever Python is running this server.
(Historically this hunted a conda env via build.json; conda is gone since old_jazz.)
Override with COUPLER_PYTHON if a downstream needs a different interpreter.
"""
return os.environ.get("COUPLER_PYTHON", sys.executable)
def build_app() -> FastMCP:
python = _notes_python()
build = _load_build()
version = build.get("version", "unknown")
print(f" coupler/server starting", file=sys.stderr)
print(f" jazz version : {version}", file=sys.stderr)
print(f" notes python : {python}", file=sys.stderr)
print(f" port : {PORT}", file=sys.stderr)
app = FastMCP("coupler")
# Plain HTTP liveness/readiness endpoint (no MCP session needed). Lets a k8s
# readiness probe / reverse proxy confirm the server is up without opening an
# SSE stream. Mirrors the POC stub's /health contract.
from starlette.responses import JSONResponse as _JSONResponse
@app.custom_route("/health", methods=["GET"])
async def _health(_request):
return _JSONResponse({"status": "ok", "role": "coupler",
"version": version, "transport": "sse"})
_tel = telemetry("coupler", version=version)
_tel.emit("build_app", status="ok", port=PORT) # app constructed; lifecycle start is in main()
# track which services mounted successfully for the status tool
_services: dict[str, str] = {} # name → "ok" | "down" | "off"
disabled = _disabled_services()
if disabled:
print(f" disabled : {', '.join(sorted(disabled))}", file=sys.stderr)
# ── jazz service proxies ─────────────────────────────────────────────────
for name, module in JAZZ_SERVICES:
if name in disabled:
_services[name] = "off"
_tel.emit("mount", status="off", service=name, module=module)
print(f" skipped : {name} (disabled)", file=sys.stderr)
continue
try:
proxy = create_proxy(
# env={**os.environ}: the MCP stdio client strips custom vars by default,
# so downstream MCPs never saw CODEBEAN_ROOT etc. (BUG-0002). Pass it through.
Client(StdioTransport(command=python, args=["-m", module],
env={**os.environ}))
)
app.mount(proxy, namespace=name)
_services[name] = "ok"
_tel.emit("mount", status="ok", service=name, module=module)
print(f" mounted : {name} ({module})", file=sys.stderr)
except Exception as e:
_services[name] = "down"
_tel.emit("mount", status="down", service=name, module=module,
error=f"{type(e).__name__}: {e}")
print(f" [warn] {name}: {e}", file=sys.stderr)
# ── status tool ──────────────────────────────────────────────────────────
@app.tool()
async def coupler_status() -> str:
"""
Check which coupler services are available right now.
Call this if a tool fails unexpectedly, before retrying, or to decide
whether to proceed with a workflow that depends on a specific service.
Returns a JSON object: {service: "ok" | "degraded" | "down"}.
"""
status = dict(_services)
# re-probe each jazz service with a quick list-tools ping (skip disabled)
for name, module in JAZZ_SERVICES:
if name in disabled:
status[name] = "off"
continue
try:
async with Client(StdioTransport(command=python,
args=["-m", module],
env={**os.environ})) as c:
await c.list_tools()
status[name] = "ok"
except Exception:
status[name] = "down"
# probe UI
try:
async with httpx.AsyncClient() as c:
r = await c.get(
os.environ.get("JSON_CONTROLS_URL", "http://127.0.0.1:8142")
+ "/pages", timeout=2.0)
status["ui"] = "ok" if r.status_code < 500 else "degraded"
except Exception:
status["ui"] = "down"
status["coupler"] = "ok"
status["jazz_version"] = version
return json.dumps(status, indent=2)
# ── why5 (ported into the coupler package; z3_mcp via forge editable) ─────
try:
from coupler.why5 import Why5, from_ci_failure
_sessions: dict = {}
@app.tool()
def why5_new(session_id: str, trigger: str = "") -> str:
"""Start a new 5-why root cause analysis session."""
_sessions[session_id] = Why5(session_id=session_id, trigger=trigger)
return f"why5 session '{session_id}' started"
@app.tool()
def why5_fact(session_id: str, name: str, value: bool) -> str:
"""Assert a known observable fact into a why5 session."""
_sessions[session_id].fact(name, value)
return f"fact '{name}' = {value}"
@app.tool()
def why5_link(session_id: str, label: str, cause: str, because: str) -> str:
"""Add a causal link: if `because` is true then `cause` must be true."""
_sessions[session_id].link(label, cause=cause, because=because)
return f"link [{label}] {because} → {cause}"
@app.tool()
def why5_check(session_id: str, save: bool = False) -> str:
"""Verify the causal chain with Z3. Optionally save to notes_db."""
w = _sessions.get(session_id)
if not w:
return f"error: session '{session_id}' not found"
result = w.check()
if save:
w.save()
return json.dumps(result, indent=2)
@app.tool()
def why5_from_ci_failure(session_id: str, branch: str, sha: str,
pytest_output: str, save: bool = False) -> str:
"""Build and verify a why5 chain from CI pytest output."""
w = from_ci_failure(session_id, branch=branch, pre_sha=sha,
pytest_output=pytest_output)
_sessions[session_id] = w
result = w.check()
if save:
w.save()
return json.dumps(result, indent=2)
print(f" mounted : why5", file=sys.stderr)
except Exception as e:
print(f" [warn] why5: {e}", file=sys.stderr)
# ── UI bridge ────────────────────────────────────────────────────────────
UI_BASE = os.environ.get("JSON_CONTROLS_URL", "http://127.0.0.1:8142")
T = 5.0
def _get(path, **p):
r = httpx.get(f"{UI_BASE}{path}", params=p, timeout=T); r.raise_for_status(); return r.json()
def _post(path, body):
r = httpx.post(f"{UI_BASE}{path}", json=body, timeout=T); r.raise_for_status(); return r.json()
def _put(path, body):
r = httpx.put(f"{UI_BASE}{path}", json=body, timeout=T); r.raise_for_status(); return r.json()
@app.tool()
def ui_set_page(config: dict) -> str:
"""Push a page config to the browser UI."""
name = config.get("name")
if not name: return "error: config must have a 'name' field"
try: _post(f"/page/{name}", config); return f"page '{name}' registered"
except Exception as e: return f"error: {e}"
@app.tool()
def ui_get_state(page: str) -> str:
"""Get the current value of every control on a UI page."""
try: return json.dumps(_get(f"/state/{page}"), indent=2)
except Exception as e: return f"error: {e}"
@app.tool()
def ui_set_value(page: str, name: str, value: str) -> str:
"""Set a control value on a UI page."""
parsed: object = value
if value.lower() == "true": parsed = True
elif value.lower() == "false": parsed = False
else:
try: parsed = int(value)
except ValueError:
try: parsed = float(value)
except ValueError: pass
try: _put(f"/state/{page}/{name}", {"value": parsed}); return f"set {page}.{name} = {parsed!r}"
except Exception as e: return f"error: {e}"
@app.tool()
def ui_list_pages() -> str:
"""List all registered UI pages."""
try:
pages = _get("/pages")
return json.dumps(pages, indent=2) if pages else "no pages registered"
except Exception as e: return f"error: {e}"
@app.tool()
def ui_add_message(page: str, control: str, role: str, text: str) -> str:
"""Append a message to a chatbox control on a UI page."""
try: _post(f"/message/{page}/{control}", {"role": role, "text": text}); return f"message added to {page}.{control}"
except Exception as e: return f"error: {e}"
@app.tool()
def ui_get_log(limit: int = 20) -> str:
"""Get recent control change events from the browser."""
try:
log = _get("/log", limit=limit)
return json.dumps(log, indent=2) if log else "no changes yet"
except Exception as e: return f"error: {e}"
print(f" mounted : ui (port 8142)", file=sys.stderr)
return app
def main():
import asyncio
parser = argparse.ArgumentParser()
parser.add_argument("--port", type=int, default=PORT)
# Bind host. Defaults to loopback so the local (blue) deployment is unchanged;
# set --host 0.0.0.0 or COUPLER_HOST=0.0.0.0 when running inside a container /
# k8s pod so the SSE endpoint is reachable through the cluster (Caddy proxy).
parser.add_argument("--host", default=os.environ.get("COUPLER_HOST", "127.0.0.1"))
args = parser.parse_args()
app = build_app()
# service_running emits service.start + periodic service.heartbeat + service.stop
with service_running("coupler", port=args.port):
asyncio.run(app.run_http_async(transport="sse", host=args.host, port=args.port))
if __name__ == "__main__":
main()
===END SOURCE===
THE CALLABLE SURFACE — every callable in the original, lowered with codebean's
`lower_node` (#183). The rewrite must present THIS surface (same names, same
parameters, same defaults) so callers of the original keep working:
_env_port(name: str, default: int) -> int
_load_build() -> dict
_disabled_services() -> set
_notes_python() -> str
build_app() -> FastMCP
main()
CAPTURED BEHAVIORAL EXEMPLARS: none. no function in this module is minable — criteria_miner mines only
self-contained pure functions (it refuses anything that mentions a
world-touching or non-deterministic name, plus methods and no-arg
functions), and nothing here qualified
This does NOT weaken the ticket's bar — it MOVES the work: you must write the
equivalence exemplars by hand from the original's behavior before rewriting, and
they are what settles this ticket.
THE STANDARD — #200's four requirements, which this ticket exists to enforce. All four are
the standard's own words; none is optional:
1. BEHAVIOR-PRESERVING, VERIFIED NOT ASSERTED — the deliverable is ENTIRELY NEW source
code (rewritten, not copied). It must reproduce the ORIGINAL's behavior, and the proof
is the exemplars above (captured via codebean lower_* #183 + criteria_miner
IO-exemplars, exactly as #200 specifies).
2. A PYTHON MODULE — regardless of the source language. This module's source is `.py`;
the rewrite is Python either way.
3. INSTALLABLE — a proper package: pyproject.toml, `pip install -e .` works, console
entry points where applicable.
4. CONTROL-CONFIG'D — NO hardcoded paths or params. A JSON/registry config object drives
it (the install.json / registry.json idiom of this repo), and per note-for-claude a
tool that cannot see its config THROWS rather than guessing a default.
ACCEPTANCE (#200's, per rewrite ticket) — this ticket settles when ALL of:
a. the new source reproduces every captured exemplar above, plus the edge cases you add
for what sampling cannot reach (paste the runs as evidence);
b. it presents the callable surface above, so existing callers keep working;
c. `pip install -e .` installs it clean from its own pyproject.toml;
d. it runs off a config object with ZERO hardcoded settings, and throws without one.
Kind is `build` (a pair: coder + adversarial tester). This ticket hangs off #200 by a
`spawned` edge, so a stop ticket aimed at #200 brakes every rewrite in this programme.
Filed mechanically by checkouts/current/congruency/tools/forge_refactor_ticketer.py under ticket #201, from the forge bare repos at
/Users/stevenpeterson/code/installed/jazz-project/config/forge/remotes. Its original source is embedded above (#346), so this ticket is complete on its own.