Ticket graphs

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.

← all ticket-graphs

ACTIVE#421 forge-refactor

Rewrite agent_tools_suite:crank_launch.py as behavior-preserving, installable, control-config'd Python [forge-rw:5bc86ca9be7e691c]

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:5bc86ca9be7e691c (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 : 8313 bytes EXTENSION : .py CANONICAL SOURCE (the location this ticket is named for): repo : agent_tools_suite path : crank_launch.py ref : HEAD (HEAD) blob : e9421956ce79f78becec3352d0ce481c3f4f6198 (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 e9421956ce79f78becec3352d0ce481c3f4f6198 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_launch.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 — 8313 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_launch — one command to start an autonomous cranking agent from a JSON job. Reads a job artifact, then: (1) ensures the jail services are up, (2) wallet-signs + files the standing policy for the job's component, (3) locks the gate-only config dir, and (4) opens a Claude terminal already seeded with the brief + this run's goal — so the agent cranks autonomously within the policy. python3 crank_launch.py [job.json] # default: ~/jail/crank_job.json python3 crank_launch.py [job.json] --dry-run # show the plan; sign nothing, launch nothing job.json: {"component": "...", "repo": "~/code/...", "config_dir": "~/.config/crank-cli", "brief": "~/jail/crankws.md", "policy": {"max_version": "0.05", "days": 1}, "goal": "..."} """ import json import hashlib import os import subprocess import sys import urllib.request from datetime import datetime, timezone HERE = os.path.dirname(os.path.abspath(__file__)) JAIL_PY = os.path.join(HERE, ".venv", "bin", "python") DEFAULT_JOB = os.path.join(HERE, "crank_job.json") ATS_ENV = os.path.expanduser("~/.secrets/ats.env") LOCKDOWN = os.path.expanduser("~/run-claude-code/lockdown.py") PROMPT_FILE = os.path.expanduser("~/bin/config/ats/crank_prompt.txt") SERVICES = ("ats_service.py", "crankd_service.py", "imgd_service.py") CONG = (os.environ.get("CONGRUENCY_URL") or "http://127.0.0.1:8899").rstrip("/") def run(*cmd, check=False): return subprocess.run([str(c) for c in cmd], check=check) def _secret(name): path = os.path.expanduser("~/.secrets/ats.env") if os.path.isfile(path): for line in open(path): if line.startswith(name + "="): return line.strip().split("=", 1)[1] return os.environ.get(name) def _canonical(spec): return json.dumps(spec, sort_keys=True, separators=(",", ":")) def _fetch_job(h): """Fetch the canonical spec for a job hash from congruency, verifying the content address.""" with urllib.request.urlopen(CONG + "/?route=job.get&hash=" + h, timeout=5) as r: rows = json.loads(r.read().decode() or "{}").get("rows") or [] if not rows: sys.exit("no job for hash %s" % h) spec = rows[0]["spec"] if hashlib.sha256(spec.encode()).hexdigest() != h: sys.exit("HASH MISMATCH — stored spec does not content-address to %s" % h) return spec def _anchor(job_hash, component, max_version, goal): """Record a run anchor stamped to the agent — links this run to the immutable job in the feed.""" body = json.dumps({"ts": datetime.now(timezone.utc).isoformat(), "session": "crankws-agent", "tool": "crank-run", "intent": "job=%s policy=%s<%s goal=%s" % (job_hash[:12], component, max_version, (goal or "")[:60]), "note": job_hash}).encode() req = urllib.request.Request(CONG + "/?api=memories", data=body, method="POST", headers={"Content-Type": "application/json", "X-Api-Key": _secret("CONGRUENCY_API_KEY") or ""}) try: urllib.request.urlopen(req, timeout=5).read() except OSError: pass def _flag(name): if name in sys.argv: i = sys.argv.index(name) return sys.argv[i + 1] if i + 1 < len(sys.argv) else None return None def main(): dry = "--dry-run" in sys.argv hash_arg = _flag("--job-hash") if hash_arg: # fetch the immutable job by hash job = json.loads(_fetch_job(hash_arg)) jh = hash_arg else: positional = [a for a in sys.argv[1:] if not a.startswith("--") and a != hash_arg] job_path = os.path.expanduser(positional[0] if positional else DEFAULT_JOB) if not os.path.isfile(job_path): sys.exit("no job artifact at %s" % job_path) job = json.load(open(job_path)) jh = hashlib.sha256(_canonical(job).encode()).hexdigest() component = job["component"] policy = job["policy"] config_dir = os.path.expanduser(job.get("config_dir", "~/.config/crank-cli")) repo = os.path.expanduser(job.get("repo", "~/code/" + component)) brief = open(os.path.expanduser(job["brief"])).read() if job.get("brief") else "" prompt = (brief + "\n\n## THIS RUN'S GOAL\n" + job.get("goal", "")).strip() mode = job.get("launch", "terminal") # "screen" (detached, named, reattachable) | "terminal" token = job.get("token", "CRANKWS").upper() # bearer scope: CRANKWS (workstation) | CRANKCONG (congruency) session = "crank-%s-%s" % (component, jh[:8]) # Two things keep the agent gate-only: (1) export ONLY the crankws token — sourcing the whole # ats.env breaks on the spaced pubkey value; (2) launch from a NEUTRAL cwd — the pre-trusted # <config_dir>/workspace, which has no project .mcp.json (unlike ~/code/.mcp.json → # git-surface/codebean/spec-graph) AND is pre-accepted in the config's trusted-projects, so the # detached agent never hangs on the "trust this folder?" dialog. lockdown --apply creates+trusts it. # The gate loads from the locked config dir's .claude.json; the agent operates on the repo by # absolute path, not via cwd. workspace = os.path.join(config_dir, "workspace") tokvar = "ATS_TOKEN_%s" % token launch = ("export {tv}=$(grep '^{tv}=' {env} | cut -d= -f2-); " 'cd {ws} && CLAUDE_CONFIG_DIR={cfg} claude "$(cat {pf})"').format( tv=tokvar, env=ATS_ENV, cfg=config_dir, ws=workspace, pf=PROMPT_FILE) if dry: print("DRY RUN — would:") print(" job hash :", jh, "(from congruency)" if hash_arg else "(local file)") print(" services up :", ", ".join(SERVICES)) print(" policy : %s max_version<%s days=%s" % (component, policy["max_version"], policy.get("days", 0))) print(" config dir :", config_dir) print(" prompt : %d chars -> %s" % (len(prompt), PROMPT_FILE)) print(" run anchor : crankws-agent · crank-run · job=%s" % jh[:12]) print(" launch mode :", mode, ("(screen session '%s')" % session) if mode == "screen" else "(Terminal.app)") print(" launch :", launch) return for svc in SERVICES: # 1. services up (idempotent) run(JAIL_PY, os.path.join(HERE, svc), "on") if run(JAIL_PY, os.path.join(HERE, "crank_policy.py"), component, # 2. sign + file the policy "--max-version", policy["max_version"], "--days", policy.get("days", 0), "--yes").returncode: sys.exit("policy step failed — aborting launch") run(JAIL_PY, LOCKDOWN, "--apply", "--config-dir", config_dir, "--token", token) # 3. gate-only config _anchor(jh, component, policy["max_version"], job.get("goal", "")) # 4. link the job into provenance os.makedirs(os.path.dirname(PROMPT_FILE), exist_ok=True) # 5. seed prompt + launch with open(PROMPT_FILE, "w") as fh: fh.write(prompt) if mode == "screen": launch_sh = os.path.join(os.path.dirname(PROMPT_FILE), "crank_launch.sh") with open(launch_sh, "w") as fh: fh.write("#!/bin/bash\n" + launch + "\n") record = job.get("record", True) log = os.path.join(os.path.dirname(PROMPT_FILE), "%s.log" % session) if record: # `script` records ALL session I/O to a typescript run("screen", "-dmS", session, "script", "-q", log, "bash", launch_sh) else: run("screen", "-dmS", session, "bash", launch_sh) print("✓ launched in screen '%s' (job %s · policy %s<%s)%s" % (session, jh[:12], component, policy["max_version"], (" · recording %s" % log) if record else "")) print(" watch: screen -r %s · list: screen -ls · stop: screen -S %s -X quit" % (session, session)) if record: print(" recorded I/O: tail -f %s" % log) else: run("osascript", "-e", 'tell application "Terminal" to do script %s' % json.dumps(launch), check=True) print("✓ launched in Terminal: job %s · policy %s<%s · seeded (%d chars)" % (jh[:12], component, policy["max_version"], len(prompt))) 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: run(*cmd, check=False) _secret(name) _canonical(spec) _fetch_job(h) _anchor(job_hash, component, max_version, goal) _flag(name) 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. 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.

patch

none

approval

unapproved

runs

no runs recorded

mandate (clauses)

not yet recorded — lands with the ticket-class build

manifestations

not yet recorded — lands with the ticket-class build

guarantees

not yet recorded — lands with the ticket-class build

journey (blunders & successes)

not yet recorded — lands with the ticket-class build

artifacts (forge)

checkouts/current/forge2/agent_tools_suite/crank_launch.py:96570f4a8b forge-source · forge ebcb3d6b718e
checkouts/current/forge2/agent_tools_suite/pyproject.toml:96570f4a8b forge-source · forge 24538cf9fa6d
checkouts/current/forge2/agent_tools_suite/registry.json:96570f4a8b forge-source · forge 475ebe666c09

source (forge tree)

browse congruency source (forge-parked tree)

children

not yet recorded — lands with the ticket-class build