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
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:d10d74a9bac32554
(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 : 4790 bytes
EXTENSION : .py
CANONICAL SOURCE (the location this ticket is named for):
repo : agent_tools_suite
path : crank_discipline.py
ref : HEAD (HEAD)
blob : 08bc6bba086630d09f1cf36a0f77f01de22a453c (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/agent_tools_suite.git cat-file blob 08bc6bba086630d09f1cf36a0f77f01de22a453c
THE SAME MODULE ALSO LIVES AT THESE FORGE TIPS: none — this module is unique to
agent_tools_suite among the repos scanned so far.
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-group/jazz-forge/agent_tools_suite:crank_discipline.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 — 4790 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
"""crank_discipline — the single, inescapable gate every crank routes through. OWNED BY ~/jail.
A crank (a versioned, tagged state change) is allowed only when:
1. an OPEN memory ticket exists for the component -> else CrankRefused
2. its intent + provenance is recorded to congruency -> HARD; unrecordable => refused
3. its patch runs CONFINED under the audit-veto -> python-not-bash, no escape
Imported by every repo's ratchet as a HARD DEPENDENCY: a ratchet that cannot import this cannot crank.
Stdlib-only, so any repo's interpreter can import it regardless of that repo's venv:
import sys, os
sys.path.insert(0, os.path.expanduser("~/jail"))
import crank_discipline as discipline
discipline.gate(component="workstation", intent=message) # ticket + provenance, or raise
discipline.run_confined(patch, repo_root) # the audit-veto
"""
import json
import os
import subprocess
import sys
import urllib.request
from datetime import datetime, timezone
HERE = os.path.dirname(os.path.abspath(__file__))
GUARD = os.path.join(HERE, "audit_guard.py")
CONGRUENCY = (os.environ.get("CONGRUENCY_URL") or "http://127.0.0.1:8899").rstrip("/")
PROVENANCE_LOG = os.path.expanduser("~/bin/config/crank/provenance.jsonl")
class CrankRefused(RuntimeError):
"""A crank precondition was not met — the crank is refused before it can change state."""
def _now():
return datetime.now(timezone.utc).isoformat()
def _congruency(method, path, payload=None):
data = json.dumps(payload).encode() if payload is not None else None
request = urllib.request.Request(CONGRUENCY + path, data=data, method=method,
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(request, timeout=3) as response:
return json.loads(response.read().decode() or "{}")
def has_open_ticket(component):
"""True/False if congruency is reachable; RAISES OSError if it is not (ticket unverifiable).
The raise lets the gate tell 'up but no ticket' (refuse) apart from 'down' (proceed best-effort)."""
query = f"/?api=tickets&component={component}&status=OPEN&order=id.desc&per=1"
return bool(_congruency("GET", query).get("rows"))
def record(component, intent, session):
"""Best-effort provenance: ALWAYS to the local jail log, and to congruency when reachable.
Returns the stamp. Never a refusal reason — provenance degrades gracefully on outage."""
stamp = {"ts": _now(), "kind": "crank", "component": component, "intent": intent, "session": session}
try:
os.makedirs(os.path.dirname(PROVENANCE_LOG), exist_ok=True)
with open(PROVENANCE_LOG, "a") as handle:
handle.write(json.dumps(stamp) + "\n")
except OSError:
pass
try:
_congruency("POST", "/?api=memories", stamp)
except OSError:
pass
return stamp
def gate(component, intent, session=None, require_ticket=True):
"""The precondition EVERY crank must pass. Raises CrankRefused. Returns the provenance stamp.
Ticket is HARD when congruency is reachable; if congruency is DOWN the ticket can't be verified,
so the crank proceeds (provenance falls back to the local log). Confinement (run_confined) is
always hard, independent of congruency."""
if not intent:
raise CrankRefused("crank discipline: an 'intent' is required (why are you cranking?)")
session = session or os.environ.get("CLAUDE_CODE_SESSION_ID") or f"crank-{os.getpid()}"
if require_ticket:
try:
if not has_open_ticket(component):
raise CrankRefused(f"crank discipline: no OPEN memory ticket for '{component}' — file one first")
except OSError:
pass # congruency unreachable -> ticket unverifiable -> proceed (provenance best-effort)
return record(component, intent, session)
def run_confined(patch, repo_root, timeout=300):
"""Run a crank patch under the audit-veto. Raises CrankRefused on escape/failure. Returns stdout tail."""
if not os.path.isfile(GUARD):
raise CrankRefused(f"crank discipline: audit_guard missing at {GUARD} — cannot confine the patch")
env = {**os.environ, "CRANK_REPO": os.path.realpath(repo_root), "PYTHONDONTWRITEBYTECODE": "1"}
result = subprocess.run([sys.executable, GUARD, patch], cwd=repo_root, capture_output=True,
text=True, timeout=timeout, env=env)
if result.returncode != 0:
raise CrankRefused("crank discipline: patch refused/failed under the audit-veto "
f"(rc={result.returncode}): {(result.stderr or result.stdout).strip()[-400:]}")
return (result.stdout or "").strip().splitlines()[-3:]
===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:
_now()
_congruency(method, path, payload=None)
has_open_ticket(component)
record(component, intent, session)
gate(component, intent, session=None, require_ticket=True)
run_confined(patch, repo_root, timeout=300)
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. WRITE ONLY YOUR OWN MODULE FILE at
forge2/<repo>/<module>.py. Do NOT hand-author pyproject.toml or __init__.py: the
harness DERIVES them ADDITIVELY from the modules present (congruency/tools/
forge_package.py, #410 — the analogue of the tag-registry fix #281), so your unit never
conflicts with, or clobbers, a sibling module of the same forge2/<repo>/ package. A
package-level pyproject includes every .py in the directory, so your module installs
and imports as `<repo>.<module>` the moment it lands.
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. Add ONLY your
own `forge2.<repo>.<module>` section to the package's registry.json (create the file if
it is absent); NEVER read, edit or drop a sibling module's section — the sections are
disjoint keys and stay additive that way.
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.