Problem
The harness works as a template or has to be hand-wired into existing projects
- Check commands are hardcoded in:
harness/gate.py (COMMIT_CHECKS/FULL_CHECKS) and .github/workflows/ci.yml, with a parity test policing the drift instead of eliminating drift.
pylint src harness, --cov-fail-under=100, and whole-repo ruff/complexipy/semgrep make most pre-existing repo fails day one -- lots for agents to fix before they can commit, not alway desired.
- Every check runs via
uv run --no-sync, assuming a uv-managed synced venv.
- Git hooks hardcode
.venv/bin/harness and harness install silently overwrites core.hooksPath killing a host's existing hooks
- The forbidden-paths policy is duplicated as prose in
docs/PROMPT.md and AGENTS.md, allowing for potential drift from gate.py.
Design
[tool.ralph] in pyproject.toml becomes the single place humans set policy.
gate.py reads it at runtime
- absent/empty config = today's exact strict behavior. Defaults stay as constants in
gate.py, which also carry the safety minimum. pyproject.toml itself is always forbidden to agents, under any config. CI and hooks stop carrying policy, they just invoke harness gate.
[tool.ralph]
# each key REPLACES its default wholesale; absent key = strict default
forbidden-dirs = [...]
forbidden-files = [...]
forbidden-patterns = [...]
push-branch = true # false: pre-push gate blocks agent pushes
prompt = "docs/PROMPT.md" # what `harness run` feeds the agent
[tool.ralph.commit-checks]
"ruff lint" = ["uvx", "ruff", "check", "src/mypkg"]
[tool.ralph.full-checks]
pytest = ["poetry", "run", "pytest", "--cov=src/mypkg", "--cov-fail-under=100"]
[tool.ralph.roles.frontend] # per-agent scopes; globals always apply on top
allowed = ["frontend/*"]
forbidden = ["backend/*"]
Under RALPH_LOOP the config is read from HEAD (git show HEAD:pyproject.toml), not the working tree so an agent editing pyproject without staging it cannot weaken the gate. Malformed config is not validated, it fails loudly (uncaught exception, hook blocks).
Three supported forms:
- Greenfield template git clone
harness install <name> byte-identical to today.
- Drop-in
uvx ralph-harness install in an existing repo. Writes only missing files, never touches [project]/version/tool tables, chains existing hooks.
- PyPI dependency.
pip install ralph-harness: harness on PATH. Ships inside the wheel.
Tasks
- Collapse ci.yml to one step
uv run --no-sync harness gate (keep dependency-review, uv sync, color env).
- Delete the two parity tests, add:
ci.yml invokes harness gate (or vice versa) + contains no direct tool invocations.
gate.py reads [tool.ralph] for one ralph_policy(repo) function (inline tomllib, no config module/dataclass/validation)
- HEAD-read under RALPH_LOOP with CalledProcessError fallback to defaults.
- Casefold config sets
- tuple the dirs (startswith)
- union {"pyproject.toml"} floor
- push-branch enforcement in run_gate
- containment ejects staged paths outside the role's allowed/inside its forbidden (fnmatch globs)
- unknown role fails loudly
- Hooks become chaining dispatchers: resolve
.venv/bin/harness else PATH harness, run with < /dev/null, then if [ -x "$(git rev-parse --git-common-dir)/hooks/<name>" ]; then exec ...; fi.
harness install command extended (name kept, NAME optional).
- Current greenfield template
[project].name == "ralph-harness"
- Drop-in branch: write-if-absent the full loop contract (.githooks/*, docs/PROMPT.md, AGENTS.md, ralph_preferences.py, docs/plan.md, docs/specs/base.md, docs/PROJECT_STATUS.md). Append a real
[tool.ralph] header + commented examples (tomllib to detect, plain text append to write. Idempotent, byte-diff = block only)
- smart suggestions from a one-time read-only peek (poetry.lock, poetry-run pytest example, package dirs for lint paths) as comments only
- wire_hooks never clobbers a foreign core.hooksPath (prints invoke lines, prints a repo: local block when
.pre-commit-config.yaml exists).
- Scaffold sources resolve repo-first, else importlib.resources (wheel force-include data doesn't exist on disk in editable installs). Wheel: exclude harness/tests, add ralph-harness script alias.
- Host-editable preferences: install copies
harness/preferences.py repo-root ralph_preferences.py, gate loads it via importlib.util.spec_from_file_location, filename joins default FORBIDDEN_FILES.
harness run --prompt <string|path> --role <name> + graceful "need a prompt" exit 2.
- Policy footer injection: run() appends the live policy (forbidden sets, push-branch state, role scope) from ralph_policy() to the prompt; delete driftable hardcoded lists from docs/PROMPT.md and AGENTS.md, agents see exactly what the gate enforces.
- publish.yml guard if github.repository == 'rxdt/py_ralph_frame'.
Non-goals
No config module/dataclass/validation layer.
No merge/extend config semantics (wholesale replace only).
No auto-ratchet or baseline files.
No copier/cruft.
No runtime package-manager detection.
No hook renaming/backups.
No --force flags.
No CLI coverage override (100 stays the default and hosts set their own number in their own toml).
Acceptance
Template with no [tool.ralph]: every dispatched argv identical to today's constants (pinned by tests).
Drop-in rehearsal: build wheel to poetry-shaped scratch repo with a pre-existing hook to uvx --from dist/*.whl ralph-harness install to commit runs harness-then-host-hook
[tool.ralph] override honored
RALPH_LOOP=1 ejects a staged pyproject edit while honoring HEAD policy
loop commit, push survives
- PyPI rehearsal:
pip install wheel in a scratch venv to install scaffolds from package data to gate runs
- harness gate green (100% coverage, complexipy ≤ 10, all ruff caps) after every task.
Problem
The harness works as a template or has to be hand-wired into existing projects
harness/gate.py(COMMIT_CHECKS/FULL_CHECKS) and.github/workflows/ci.yml, with a parity test policing the drift instead of eliminating drift.pylint src harness, --cov-fail-under=100, and whole-repo ruff/complexipy/semgrep make most pre-existing repo fails day one -- lots for agents to fix before they can commit, not alway desired.uv run --no-sync, assuming a uv-managed syncedvenv..venv/bin/harnessandharness installsilently overwritescore.hooksPathkilling a host's existing hooksdocs/PROMPT.mdandAGENTS.md, allowing for potential drift from gate.py.Design
[tool.ralph]inpyproject.tomlbecomes the single place humans set policy.gate.pyreads it at runtimegate.py, which also carry the safety minimum.pyproject.tomlitself is always forbidden to agents, under any config. CI and hooks stop carrying policy, they just invokeharness gate.Under
RALPH_LOOPthe config is read from HEAD (git show HEAD:pyproject.toml), not the working tree so an agent editing pyproject without staging it cannot weaken the gate. Malformed config is not validated, it fails loudly (uncaught exception, hook blocks).Three supported forms:
harness install <name>byte-identical to today.uvx ralph-harness installin an existing repo. Writes only missing files, never touches[project]/version/tooltables, chains existing hooks.pip install ralph-harness: harness onPATH. Ships inside the wheel.Tasks
uv run --no-sync harness gate(keep dependency-review, uv sync, color env).ci.ymlinvokes harness gate (or vice versa) + contains no direct tool invocations.gate.pyreads[tool.ralph]for one ralph_policy(repo) function (inline tomllib, no config module/dataclass/validation).venv/bin/harness else PATH harness, run with< /dev/null,thenif [ -x "$(git rev-parse --git-common-dir)/hooks/<name>" ]; then exec ...; fi.harness installcommand extended (name kept, NAME optional).[project].name == "ralph-harness"[tool.ralph]header + commented examples (tomllib to detect, plain text append to write. Idempotent, byte-diff = block only).pre-commit-config.yamlexists).harness/preferences.pyrepo-rootralph_preferences.py, gate loads it via importlib.util.spec_from_file_location, filename joins defaultFORBIDDEN_FILES.harness run --prompt <string|path> --role <name>+ graceful "need a prompt" exit 2.Non-goals
No config module/dataclass/validation layer.
No merge/extend config semantics (wholesale replace only).
No auto-ratchet or baseline files.
No copier/cruft.
No runtime package-manager detection.
No hook renaming/backups.
No --force flags.
No CLI coverage override (100 stays the default and hosts set their own number in their own toml).
Acceptance
Template with no [tool.ralph]: every dispatched argv identical to today's constants (pinned by tests).
Drop-in rehearsal: build wheel to poetry-shaped scratch repo with a pre-existing hook to
uvx --from dist/*.whl ralph-harness installto commit runs harness-then-host-hook[tool.ralph] override honored
RALPH_LOOP=1 ejects a staged pyproject edit while honoring HEAD policy
loop commit, push survives
pip install wheelin a scratch venv to install scaffolds from package data to gate runs