refactor(tooling): T-1286 — generate, pr and dev become reach domains
Twelve scripts retired, three domains registered. `reach` now covers nine. generate: `generate-brands` and `generate-corporations` were the second and third copies of the same 24-line build-if-missing-then-exec bash `tooling/atlas` carried, so they collapsed into `core.process.cargo_binary` rather than being ported. `import_economics` shelled out to the first of those, so it now calls that helper — `generated_brands.toml` comes back byte-identical, and the stamp registry swaps the retired wrapper for `core/process.py`. pr: `watchlist-diff` derives its watched set from `generator_sources.py` instead of restating it, so it cannot drift from the stamp check. dev: the environment scripts split decision from performing, per D-263's guarded-exec rule. `godot_plan()` and `worktree_plan()` decide what would happen; `install_godot()`, `install_rust()` and `setup_worktree()` do it. `tooling/test_environment.py` pins the version pin, both override precedences, the already-current skip, the platform refusal and both worktree refusals — none of them performed. `make setup` now installs reach first, since the targets that install rust and godot are reach verbs. Two live bugs found while porting: - The clerk read its decision index from `decisions/README.md`, a path that stopped existing when the DQR tree moved to `governance/`. Every clerk agent has been grepping blind; its prompt pointed at the same dead directory. - The conformance exec-check matched any `x.system()` regardless of receiver, so `platform.system()` read as `os.system()`. Narrowed and re-proved against a real mutant. `process.run` gains `input=`, `timeout=` and a `ProcessTimeout` subclass so a killed run stays distinguishable from a verdict. The pre-push hook no longer merges the clerk's stderr into its stdout — under streaming the last merged line is a JSONL event, which would read as an unrecognised verdict and block. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,16 @@ def meta_path(job_id: str) -> Path:
|
||||
return jobs_dir() / f"{job_id}.json"
|
||||
|
||||
|
||||
class ProcessTimeout(ReachError):
|
||||
"""A run that exceeded its `timeout=`.
|
||||
|
||||
Its own class because a timeout is not a failure verdict — a caller that
|
||||
treats "the tool ran and said no" and "the tool never answered" alike will
|
||||
turn a slow machine into a rejection. Uncaught it still reads as a
|
||||
`ReachError` with a remedy.
|
||||
"""
|
||||
|
||||
|
||||
def run(
|
||||
argv: list[str],
|
||||
*,
|
||||
@@ -70,6 +80,8 @@ def run(
|
||||
env: dict[str, str] | None = None,
|
||||
capture: bool = True,
|
||||
check: bool = True,
|
||||
input: str | None = None,
|
||||
timeout: float | None = None,
|
||||
fix: str | None = None,
|
||||
missing_fix: str | None = None,
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
@@ -108,12 +120,20 @@ def run(
|
||||
capture_output=capture,
|
||||
text=True,
|
||||
shell=False,
|
||||
input=input,
|
||||
timeout=timeout,
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
raise ReachError(
|
||||
f"{argv[0]} is not installed or not on PATH",
|
||||
fix=missing_fix or f"install {argv[0]}, or check PATH in a non-interactive shell",
|
||||
) from exc
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
raise ProcessTimeout(
|
||||
f"{argv[0]} did not finish within {timeout:g}s",
|
||||
fix=f"re-run, or raise the timeout — `{' '.join(argv)}` was killed, "
|
||||
"which says nothing about whether it would have succeeded",
|
||||
) from exc
|
||||
|
||||
if check and result.returncode != 0:
|
||||
detail = (result.stderr or result.stdout or "").strip()
|
||||
@@ -126,6 +146,47 @@ def run(
|
||||
return result
|
||||
|
||||
|
||||
def cargo_binary(name: str, *args: str, capture: bool = True) -> str:
|
||||
"""Run a Rust binary from `server/`, building it first if it is absent.
|
||||
|
||||
Three scripts wrote this by hand — `tooling/atlas`, `generate-brands`,
|
||||
`generate-corporations` — each ~24 lines of identical bash: check for the
|
||||
debug binary, `cargo build --bin` it if missing, `exec` it with every
|
||||
argument. Three copies of one idea is a substrate, so it lives here.
|
||||
|
||||
Build-if-missing is kept because it is genuinely useful on a cold checkout,
|
||||
but it is announced rather than silent: a first run that takes ninety
|
||||
seconds with no explanation reads as a hang.
|
||||
"""
|
||||
binary = config.path("server", "target", "debug", name)
|
||||
if not binary.is_file():
|
||||
from tooling.core import console
|
||||
|
||||
console.event(f"building the {name} binary (first run)", level="warn")
|
||||
build = run(
|
||||
["cargo", "build", "--bin", name],
|
||||
cwd=config.path("server"),
|
||||
check=False,
|
||||
missing_fix="install Rust — make setup-rust",
|
||||
)
|
||||
if build.returncode != 0:
|
||||
raise ReachError(
|
||||
f"could not build {name}\n{(build.stderr or '').strip()}",
|
||||
fix=f"cd server && cargo build --bin {name} — for the full error",
|
||||
exit_code=build.returncode,
|
||||
)
|
||||
|
||||
result = run([str(binary), *args], check=False, capture=capture)
|
||||
if result.returncode != 0:
|
||||
raise ReachError(
|
||||
f"{name} {' '.join(args)} exited {result.returncode}\n"
|
||||
+ (result.stderr or "").strip(),
|
||||
fix=f"run `{binary} {' '.join(args)}` directly for the full output",
|
||||
exit_code=result.returncode,
|
||||
)
|
||||
return result.stdout or ""
|
||||
|
||||
|
||||
def spawn_detached(argv: list[str]) -> str:
|
||||
"""Run `reach <argv>` in a detached child. Returns the job id immediately.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user