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 #297 — 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:ad7c8896d4d55d8d
(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 : 13999 bytes
EXTENSION : .py
CANONICAL SOURCE (the location this ticket is named for):
repo : coupler
path : coupler/server.py
ref : HEAD (as recorded when this module was first filed)
blob : fe6aaa84331ef0616cca49f1a72c218ba4c46e66 (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/coupler.git cat-file blob fe6aaa84331ef0616cca49f1a72c218ba4c46e66
THE SAME MODULE ALSO LIVES AT THESE FORGE TIPS — 1 copy/copies. They SHARE THIS
ticket: this module is rewritten ONCE, and every location below is satisfied by
that one rewrite. Do not file a ticket per copy.
gitlab__testmonkeyalpha__coupler:coupler/server.py
HISTORICAL OCCURRENCES of this exact blob — every OTHER DISTINCT {repo, path}
the omni-git index (`occ`, joined on the git blob sha1) records this
byte-identical file at, with the index's own `ts` for the first row of each.
Any place this run enumerated LIVE is listed under the forge tips above, not
here. Context for the rewrite, not extra work:
testmonkeyalpha/coupler:coupler/server.py [occ.ts gitlab]
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 — 13999 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 ─────────────────────────────────────────────────────────────────
sys.path.insert(0, str(JAZZ_DEV / "jazz"))
try:
from agent.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.