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:fb5b78f7895ed87b
(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 : 3041 bytes
EXTENSION : .py
CANONICAL SOURCE (the location this ticket is named for):
repo : agent_tools_suite
path : deploy_test_service.py
ref : HEAD (HEAD)
blob : 248ee67b26278ade123429900a79fb701f679a25 (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 248ee67b26278ade123429900a79fb701f679a25
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 (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 — 3041 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
"""deploy_test_service — start/stop the autonomous deploy+test loop as a plain user process.
python3 deploy_test_service.py on # start the deploy_test poll loop in the background
python3 deploy_test_service.py off # stop it
python3 deploy_test_service.py status # running? + the loop's mint-vs-deployed view
Mirrors crankd_service.py / ats_service.py. Not a daemon and not launchd — it runs only while you
want it. `on` detaches the process (start_new_session) so the loop outlives this launcher; `off`
stops it. State is a pidfile. The loop itself (`deploy_test.py loop`) watches the mint tree for a
newly-minted version-* tag and, fully autonomously, preflights → snapshots → pushes → deploys →
tests it live. See deploy_test.py for the full contract and its config env vars.
"""
import os
import signal
import subprocess
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
PYTHON = os.path.join(HERE, ".venv", "bin", "python") # the jail's own venv, beside this script
LOOP = os.path.join(HERE, "deploy_test.py")
PIDFILE = os.path.expanduser("~/bin/config/deploy_test/deploy_test.pid")
LOGFILE = os.path.expanduser("~/bin/logs/deploy_test/loop.out.log")
def read_pid():
"""The pid in PIDFILE if that process is alive, else None (clearing a stale pidfile)."""
try:
pid = int(open(PIDFILE).read().strip())
except (OSError, ValueError):
return None
try:
os.kill(pid, 0) # signal 0 is a liveness probe; it does not touch the process
except OSError:
os.remove(PIDFILE)
return None
return pid
def on():
running = read_pid()
if running:
print(f"already running (pid {running})")
return
os.makedirs(os.path.dirname(PIDFILE), exist_ok=True)
os.makedirs(os.path.dirname(LOGFILE), exist_ok=True)
logfile = open(LOGFILE, "a")
process = subprocess.Popen(
[PYTHON, "-u", LOOP, "loop"], # -u: unbuffered, so the detached log stays live
stdout=logfile,
stderr=subprocess.STDOUT,
start_new_session=True, # detach so the loop outlives this launcher
)
with open(PIDFILE, "w") as handle:
handle.write(str(process.pid))
print(f"started (pid {process.pid}) — log: {LOGFILE}")
def off():
pid = read_pid()
if not pid:
print("not running")
return
os.kill(pid, signal.SIGTERM)
os.remove(PIDFILE)
print(f"stopped (pid {pid})")
def status():
pid = read_pid()
print(f"process : {f'running (pid {pid})' if pid else 'stopped'}")
# the loop is a poller, not a server — surface its own mint-vs-deployed view.
subprocess.run([PYTHON, LOOP, "status"])
COMMANDS = {"on": on, "off": off, "status": status}
def main():
command = sys.argv[1] if len(sys.argv) > 1 else "status"
action = COMMANDS.get(command)
if action is None:
sys.exit("usage: deploy_test_service.py [on|off|status]")
action()
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:
read_pid()
on()
off()
status()
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.