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:
2026-09-02 17:00:46 +02:00
co-authored by Claude Opus 5
parent a45415b23a
commit 338644b409
41 changed files with 1183 additions and 615 deletions
+61
View File
@@ -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.