gah is a CLI that runs coding agents against real repositories with guardrails around git worktrees, validation, pushing, draft MR/PR creation, PM ticket decomposition, session logging, and cleanup.
Pre-built binaries are published to GitHub Releases. A GitHub token with repo read scope is required because the repository is private.
$env:GITHUB_TOKEN="ghp_xxxxxxxxxxxx"
irm "https://raw.githubusercontent.com/Kh1ng/git-agent-harness/main/scripts/install-windows.ps1" | iexSmoke test: GAH Worker appears in the system tray after install.
export GITHUB_TOKEN="ghp_xxxxxxxxxxxx"
ASSET_ID=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/Kh1ng/git-agent-harness/releases/latest" \
| python3 -c "import sys,json; a=[x for x in json.load(sys.stdin)['assets'] if x['name'].endswith('.AppImage')]; print(a[0]['id'])")
curl -L -H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/octet-stream" \
"https://api.github.com/repos/Kh1ng/git-agent-harness/releases/assets/$ASSET_ID" \
-o GAH-Worker.AppImage
chmod +x GAH-Worker.AppImage && ./GAH-Worker.AppImageSmoke test: GAH Worker appears in the system tray.
The native Android app is a control-only WebView for a central GAH node. Build it with gradle -p apps/android testDebugUnitTest assembleDebug, or download gah-android-debug-apk from the Android workflow. See Android controller.
export GITHUB_TOKEN="ghp_xxxxxxxxxxxx"
# Linux: gah-linux-x86_64 | macOS: gah-macos-universal
ASSET_NAME="gah-linux-x86_64"
ASSET_ID=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/Kh1ng/git-agent-harness/releases/latest" \
| python3 -c "import sys,json; a=[x for x in json.load(sys.stdin)['assets'] if x['name']=='$ASSET_NAME']; print(a[0]['id'])")
curl -L -H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/octet-stream" \
"https://api.github.com/repos/Kh1ng/git-agent-harness/releases/assets/$ASSET_ID" \
-o /usr/local/bin/gah && chmod +x /usr/local/bin/gahSmoke test:
gah --help
gah doctorgit- Rust toolchain (
cargoandrustup) - One backend CLI:
codexclaudeopenhands
- Provider tooling:
- GitHub:
gh - GitLab:
glabfor provider API access, plus token env vars for pushes
- GitHub:
For the Windows Tauri app and WSL2 worker, see Windows node setup. For the current capability inventory, device roles, and QR pairing, see Control surfaces.
Install the CLI and control-plane server through the deterministic host installer from a clean checkout of the default branch:
scripts/install.sh
mkdir -p ~/.config/gah
cp config/gah-config.example.toml ~/.config/gah/config.tomlscripts/install.sh detects the OS and runs scripts/install-linux.sh
(systemd) or scripts/install-macos.sh (launchd). Both accept
GAH_NODE_ROLE=central|worker; Linux defaults to central and macOS defaults to
worker. The macOS desktop can change the role later from Settings. Its local
central service uses port 3774 by default so it can coexist with T3 Code on
3773; set GAH_DESKTOP_SERVER_PORT during install to choose another port.
Each macOS update also replaces ~/Applications/GAH.app after a successful
native build. A failed replacement restores the previous app.
For a roaming worker using GAH_GATEWAY_MODE=remote, the gateway URL must
name the central/gateway node by its tailnet IP or MagicDNS name, never a LAN
IP or the worker's own tailscale ip -4. If GAH_GATEWAY_URL is omitted, the
installer reuses the host from registry_central_url; a machine with no
central config must use the Settings Reveal setup command or provide the
URL explicitly. See docs/OPERATIONS.md for the gateway setup details.
On a fresh Linux central install, the control-plane server binds its Tailscale
IPv4 address when one is available. Otherwise, it binds 127.0.0.1. Override
that choice before first install with GAH_SERVER_HOST:
GAH_SERVER_HOST=127.0.0.1 scripts/install.shThis writes /etc/gah/server.env, read by packaging/systemd/gah-server.service
via EnvironmentFile=. Edit that file's HOST= value at any time to change
the bind address without touching the installed unit; reinstalls and gah update --restart-server never overwrite an existing /etc/gah/server.env.
See docs/OPERATIONS.md for details, including the startup warning emitted
whenever the server binds a non-loopback address.
For the preferred tailnet setup, enable MagicDNS, name the central node
hermesagent, and put Tailscale Serve in front of a loopback-bound server.
Use the full https://hermesagent.<tailnet-name>.ts.net address; Tailscale
does not issue certificates for the bare MagicDNS name. Direct HTTP to the
detected tailnet address is an explicit compatibility mode: it requires
GAH_ALLOW_INSECURE_HTTP=1 and still requires a bearer token or paired-device
credential. See Tailscale names and HTTPS.
For every deployed upgrade, use the installed CLI to update the checkout,
replace the executable selected by PATH, rebuild the server, and restart the
system service only after all build steps succeed:
gah update --repo /path/to/git-agent-harness --restart-servercargo build --release is a development build only. It updates
target/release/gah; it does not replace the Cargo-installed gah executable
or rebuild/restart the control-plane server.
The default product installation covers the Rust CLI and Node control-plane server. Web, desktop, mobile, and other clients are separate packages with independent build/deployment workflows.
Run the real Vite frontend against the stateful, in-memory mock control plane:
npm ci --include=dev
npm run dev:mockOpen http://localhost:3000 and choose Chat. The mock reuses the committed
apps/server/tests/fixtures/gah/responses dashboard data and implements the
production REST and WebSocket types from @git-agent-harness/contracts. It
does not import the production server, provider adapters, worktree helpers, or
state stores, so it cannot call a provider, create a worktree, or write
production state. The normal production server and web build have no mock
route or runtime mode switch; the mock entry point lives outside
apps/server/src.
List scenarios, select one, or reset its in-memory state without restarting Vite or the repository:
curl http://127.0.0.1:3774/api/mock/scenarios
curl -X POST -H 'content-type: application/json' \
-d '{"name":"reconnect-permission"}' \
http://127.0.0.1:3774/api/mock/scenario
curl -X POST http://127.0.0.1:3774/api/mock/resetYou can also choose the initial scenario when starting development:
npm run dev:mock -- --scenario models-agyNamed scenarios are normal, slow-cancel-steer, reconnect-stream,
reconnect-permission, archive-success, archive-failure,
preview-unavailable, preview-available, preview-blocked, preview-error,
models-success, models-empty, models-delayed, models-failure,
models-agy, rest-error, and ws-error. Scenario selection restores fixed
seed data for chat, profiles, the gateway, Git, the loop, and updates. The
Playwright manager-chat control-plane spec uses these same
endpoints and the same real HTTP/WebSocket server; it does not install browser
route mocks.
Provider-specific examples:
config/gah-config.github.example.tomlconfig/gah-config.gitlab.example.tomlconfig/gah-config.gitlab-self-hosted.example.toml
- Per-run session directories under each profile
artifact_root/sessions/ - Worktrees under
defaults.worktree_base - A JSONL session ledger at:
$GAH_LEDGER_PATH, if set- otherwise
defaults.artifact_root/ledger.jsonl, if configured - otherwise
~/.config/gah/ledger.jsonl
For shared agent memory, see memory hook setup.
GAH loads config from:
--configGAH_CONFIG~/.config/gah/config.toml
Minimal shape:
[defaults]
artifact_root = "/home/you/.local/share/gah/artifacts"
worktree_base = "/home/you/.local/share/gah/worktrees"
llm_base_url = "http://localhost:4000"
llm_model_local = "your/local-model"
llm_model_cloud = "your/cloud-model"
[profiles.my-repo]
display_name = "My Repo"
repo_id = "my-repo"
provider = "github"
repo = "owner/repo"
local_path = "/path/to/local/clone"
artifact_root = "/home/you/.local/share/gah/artifacts/my-repo"
default_target_branch = "main"
validation_commands = []
# Review inactivity is a stall; continuous progress may run longer.
review_timeout_seconds = 300
# Optional independent wall-clock ceiling; omit to disable.
review_hard_timeout_seconds = 3600GitLab adds:
provider_api_base = "https://gitlab.example.com/api/v4"
provider_project_id = "12345"Secrets do not go in config.
Routing precedence:
- explicit CLI backend/model override
- profile routing config
- global
defaults.routing - built-in fallback
Example:
[defaults.routing]
default_backend = "openhands"
review_backend = "claude"
allow_review_fallback = true
[profiles.my-repo.routing]
pm_backend = "claude"
improve_backend = "codex"
# NEEDS_FIX is repaired this many times before human escalation.
max_fix_attempts_per_mr = 3Unless max_review_cycles_per_ticket is explicitly set too, GAH permits one
additional review beyond that repair cap: initial review, up to the configured
repairs, then a review of the final repair. This prevents a review budget from
silently cutting short the repair budget. The cap bounds routine reviews; each
explicit escalatory_reviewers backend/model retains one bounded attempt after
the cap so exhausted weak/routine cycles cannot skip every strong second
opinion. Paid escalation still requires its independent approval and budget.
weak_review_backend is legacy compatibility configuration. Do not use it
for normal review routing: use the ordered review_candidates pool and
escalatory_reviewers. A weak reviewer approval requires human attention;
a weak reviewer NEEDS_FIX consumes the same post-review repair budget as
any other NEEDS_FIX verdict.
For a profile that should use several subscription-backed workers, configure
explicit backend/model pairs in priority order. Higher priority wins; an
unavailable backend is skipped and the next eligible candidate is selected.
Candidates with the same priority form a balanced pool: GAH selects the pair
with the fewest executions in the last seven days (configuration order breaks
ties). After a genuine capability failure, the same work item advances past
backend/model pairs it already tried instead of selecting them again.
Do not use model = "default" for a route you care about: a backend default
can resolve through a global alias such as defaults.llm_model_cloud.
[profiles.my-repo.routing]
# Scalar fields are the legacy/default route. Keep them explicit too.
default_backend = "vibe"
default_model = "devstral-small"
pm_backend = "vibe"
pm_model = "devstral-small"
improve_backend = "vibe"
improve_model = "devstral-small"
review_backend = "vibe"
review_model = "mistral-medium-3.5"
allow_implementation_fallback = true
allow_review_fallback = true
max_implementation_failures_per_ticket = 8
# Preferred inexpensive implementation tier.
[[profiles.my-repo.routing.improve_candidates]]
backend = "vibe"
model = "devstral-small"
priority = 100
[[profiles.my-repo.routing.improve_candidates]]
backend = "agy"
model = "Gemini 3.5 Flash (Medium)"
priority = 100
[[profiles.my-repo.routing.improve_candidates]]
backend = "agy-second"
model = "Gemini 3.5 Flash (Medium)"
priority = 100
# Retained, but used less often than the preferred subscription tier.
[[profiles.my-repo.routing.improve_candidates]]
backend = "codex"
model = "gpt-5.4-mini"
priority = 50
[[profiles.my-repo.routing.improve_candidates]]
backend = "claude"
model = "haiku"
priority = 25
[[profiles.my-repo.routing.review_candidates]]
backend = "vibe"
model = "mistral-medium-3.5"
priority = 100Repeat the implementation candidates under pm_candidates when planning work
should use the same worker tier. Give each independent account its own backend
instance (agy and agy-second, for example), even when the provider and
model are identical; this keeps quota, availability, and usage records
separate.
For a shared, provider-neutral registry of CLI wrappers, account labels,
isolated state roots, and quota pools—with per-project overrides—see
docs/BACKEND_INSTANCE_CONFIG_MIGRATION.md.
Routing is currently configured in TOML. Verify the selected config and its prerequisites from the CLI before starting a loop:
gah profile show my-repo
gah doctor --profile my-repo --validate
gah status --profile my-repo --jsonPaid implementation routes can be kept as terminal fallbacks without granting the unattended loop permission to spend money:
[[profiles.my-repo.routing.improve_candidates]]
backend = "opencode"
model = "openai/gpt-paid-fallback"
priority = 10
included_in_quota = false
requires_approval = trueWhen all eligible non-paid routes are exhausted, GAH stops that work item and prints the exact approval command. Grant or revoke the exact backend/model pair without editing credentials or rewriting ledger history:
gah route-approval grant --profile my-repo ISSUE-42 \
--backend opencode --model openai/gpt-paid-fallback
gah route-approval revoke --profile my-repo ISSUE-42 \
--backend opencode --model openai/gpt-paid-fallbackThe dashboard settings editor and effective-route display are tracked in #149. Until that lands, inspect the profile's TOML directly and keep every production route's model explicit.
- GitHub: set
GITHUB_TOKENorGH_TOKEN - GitLab: set
GITLAB_PATorGITLAB_PAT2 - LLM proxy: set
LLM_API_KEYif needed
GAH keeps push auth in askpass; it does not embed tokens into remotes or push URLs.
gh auth login
export GITHUB_TOKEN=...
gah doctor --profile my-repoglab auth login --hostname gitlab.com
export GITLAB_PAT=...
gah doctor --profile my-repoglab auth login --hostname gitlab.example.com
export GITLAB_PAT=...Set:
provider_api_base = "https://gitlab.example.com/api/v4"
provider_project_id = "12345"Use the numeric GitLab project ID and set repo to the full namespace, such as
group/subgroup/project. GAH derives project links and push hosts from that base.
Use a GitLab installation on an HTTPS root host; subpath installations are not supported.
glab authenticates API operations on that host. Git pushes use GITLAB_PAT2 or
GITLAB_PAT, in that order, so a successful API readiness check does not verify push access.
gah init writes a starter config or appends a profile block.
gah init \
--profile my-repo \
--display-name "My Repo" \
--provider gitlab \
--repo group/project \
--local-path /path/to/repo \
--default-target-branch main \
--provider-api-base https://gitlab.example.com/api/v4 \
--provider-project-id 12345Preview without writing:
gah init ... --printCheck config and profile readiness:
gah doctor --profile my-repo
gah doctorDoctor checks:
- config loads
- repo path exists and is a git repo
- provider CLI exists
- expected provider token env vars are present
- push URL can be derived
- artifact/worktree paths are writable
docs/MANAGER_MEMORY.mdexists- generated-artifact publication patterns are valid
Issue bodies are worker-prompt input. Configure trusted humans and provider bots independently for each GitHub or GitLab profile:
[profiles.my_profile.publishing]
trusted_issue_human_authors = ["alice", "teammate-login"]
trusted_issue_bot_authors = ["project_5_bot_deadbeef"]
issue_intake_mode = "canonical_autonomous_only"
canonical_autonomous_label = "exec:autonomous"canonical_autonomous_only is opt-in and makes recurring discovery require the
canonical label. Owner-decision, blocked, and planning labels still win when
labels conflict. Explicit dispatch of a trusted but held or unlabelled issue
requires the visible --issue-intake-override flag; it never bypasses author
trust.
For backward compatibility, a GitHub profile without the new human list still
uses github_issue_author_allowlist; if neither list is configured, only the
repository owner is trusted. That compatibility field never grants GitLab
trust. GitLab project access-token users are recognized from the project-scoped
project_<project-id>_bot_* username and must still be listed exactly in
trusted_issue_bot_authors. Explicit empty lists deny that author class.
Before GAH creates or pushes a commit, it rejects newly tracked files matching
the profile's generated-artifact deny patterns. The default covers nested
node_modules, Vite/Vitest caches, coverage, language caches, build targets,
and TypeScript build-info files. Existing tracked files are not removed or
rewritten. Override the complete list per profile, or set an explicit empty
list to disable the guard:
[profiles.my_profile.publishing]
generated_artifact_deny_patterns = [
"**/node_modules/**",
"**/.vite/**",
"**/coverage/**",
"**/target/**",
"**/*.tsbuildinfo",
]The effective list is included in gah status --json, and gah doctor prints
the active policy. A match fails before commit/push with the exact path,
pattern, and policy source; GAH does not silently delete the worker's files.
Start with a dry run:
gah dispatch --profile my-repo --mode improve --dry-runThen run for real:
gah dispatch --profile my-repo --mode improve --backend codex --target "Fix flaky tests"PM report without a manager backend:
gah dispatch --profile my-repo --mode pmPM ticket decomposition:
gah dispatch --profile my-repo --mode pm --backend claude --target "#123"
gah pm publish --profile my-repo --plan artifacts/sessions/<run>/pm-plan-v1.json --dry-run
gah pm publish --profile my-repo --plan artifacts/sessions/<run>/pm-plan-v1.jsonPM mode with a target now injects preflight context before the manager runs:
- open trusted GitHub or GitLab issues
- open native PRs/MRs, including non-GAH branches
- recently merged native PRs/MRs
- existing
docs/tickets/*.md - current branch, dirty state, recent commits
- optional bounded project guidance
Project guidance is optional. By default GAH uses the first existing file from
docs/PM_GUIDANCE.md, docs/project-guidance.md,
docs/pm-guidance.md, or PM_GUIDANCE.md. Override that ordered search per
profile (or in routing defaults) when a repository uses another convention:
[profiles.my-repo.routing]
pm_guidance_paths = ["docs/PROJECT_BRIEF.md", "AGENTS.md"]If issue or PR/MR discovery fails or reaches a provider query cap, PM
decomposition stops instead of treating the missing duplicate context as an
empty backlog. The generated plan is validated and written as
pm-plan-v1.json in the dispatch session. Planning never creates provider
issues or local ticket files.
Implementation, fix, and experiment workers instead receive the bounded
docs/PROJECT_BRIEF.md and a task-specific live task pack. This deliberately
keeps mutable manager state and unrelated backlog out of worker prompts; the
written context-built.json artifact records every prompt section and its
estimated token size.
With a target, PM mode asks the manager for provider-neutral structured JSON,
validates field/count/byte/dependency/overlap bounds and dedupes it against
native issues, existing tickets, open PRs/MRs, and recently merged PRs/MRs.
The separate gah pm publish operation rechecks the source issue before every
provider write, uses a stable plan fingerprint to resume partial publication
without duplicates, and uses provider issue numbers as the only work identity.
The recurring controller performs the same two phases automatically only for trusted issues carrying a configured decomposition label. It claims the source issue before planning, resumes a previously-written plan after interruption, and records exact child issue numbers before releasing the claim. Publication does not close the source issue. Later controller snapshots read native child state and record reconciliation only after every child is terminal.
[profiles.my-repo.publishing]
pm_decomposition_labels = ["planning", "plan"]
pm_max_children = 12
pm_max_depth = 1
pm_max_attempts = 2
pm_timeout_seconds = 900pm_max_children is capped at 24, depth at 8, attempts at 10, and timeout at
two hours even if a larger value is configured. The timeout is one real
wall-clock process-group deadline shared by all planning backend attempts,
independent of the normal progress-aware
idle timeout. Generated owner-decision children never receive the canonical
autonomous label and therefore never enter normal implementation routing.
PM publication applies only labels explicitly mapped in the profile and only
when those labels already exist at the provider. Autonomous work also uses the
profile's existing canonical_autonomous_label:
[profiles.my-repo.publishing.pm_difficulty_labels]
easy = "difficulty:easy"
medium = "difficulty:medium"
hard = "difficulty:hard"
[profiles.my-repo.publishing.pm_risk_labels]
low = "risk:low"
medium = "risk:medium"
high = "risk:high"
[profiles.my-repo.publishing.pm_execution_labels]
human_required = "exec:owner-decision"
supervised = "exec:supervised"When improve or fix targets a ticket markdown file, GAH also reads ticket metadata such as difficulty, risk, recommended backend/model, affected files, and verification commands before routing the worker.
improve/fix retry failed validation up to --retries times (default 2).
Between attempts the worktree is hard-reset (git reset --hard + git clean -fd)
so each attempt starts from a pristine tree. The retry prompt is rebuilt from
the base task with only the latest failure output (retry blocks are not
accumulated — that confuses smaller models). The failed attempt's diff is
saved to sessions/<ts>/attempt-N/attempt-diff.patch before the wipe.
If the validation failure is byte-identical to the previous attempt, the run aborts early: an unchanged error means the agent's edits had no effect on it, which almost always indicates an environment or config problem (missing tool, bad validation command) that no retry can fix.
Validation commands run through sh -c, so shell syntax (cd x && y, pipes,
env vars) works.
Before attempt 1, validation runs once on the pristine worktree (baseline).
A failing baseline is recorded to sessions/<ts>/baseline-validation-failure.txt
and injected into the task prompt, so the agent knows whether a failure is
pre-existing. If the final failure is identical to the baseline, the error
message says so — the agent's changes never affected it, which means the
validation command or environment is broken, not the code.
--allow-draft-fail pushes a [DRAFT-FAIL] MR even if validation never passes.
gah dispatch --profile my-repo --mode experiment --target "research question"Runs the backend with a research prompt, collects untracked artifacts
(*.ipynb, *.html, *.png, *.csv, *.parquet) into the session dir,
asks an LLM judge whether the task was answered, and opens a draft MR only
if code changed.
Profiles may set env_file (dev credentials, loaded by default) and
env_file_prod (loaded only with --prod). Keep prod credentials out of
dev runs; --prod also switches policy enforcement to git-push-prod.
docs/gah-manager-skill.md is the system prompt / skill file for a manager
agent that orchestrates GAH: decomposes work via PM mode, dispatches workers,
tracks state in the target repo's docs/MANAGER_MEMORY.md, and escalates
failed tickets to stronger models.
The central node stores versioned skills and their backend bindings in
~/.config/gah/skills.json (GAH_SKILL_BANK_PATH overrides the path).
The Chat page's Skills control shows the resolved set for the current
project and backend. Projects inherit the canonical backend set until they
save an override; Use default restores inheritance. The exact resolved
versions are recorded in the chat event log and injected into every normal
turn.
Workers resolve bindings from the central /api/skills/resolve endpoint and
cache the last successful response under ~/.config/gah/skill-cache. A worker
uses that cache when the central node is temporarily unreachable and fails the
dispatch when neither source is usable. Resolved IDs become native --skills
arguments for Hermes and OpenHands; other runners record the binding without a
provider-specific flag. Legacy --skills values in
hermes_args and openhands_args are ignored with a warning; configure the
central binding instead.
Review mode now produces:
review-report.mdreview-verdict.json
Verdicts are:
APPROVE_STRONGAPPROVE_WEAKNEEDS_FIXREJECTHUMAN_REVIEW
Weak or fallback review always requires human review. No auto-merge is performed.
When the provider can be reached, GAH also posts a concise MR/PR comment and best-effort labels such as:
gah-ready-for-humangah-needs-fixgah-human-reviewgah-review-weak
Inspect recent runs:
tail -n 20 ~/.config/gah/ledger.jsonl
jq . ~/.config/gah/ledger.jsonl | lessFields include mode, backend, branch, session dir, validation status, commit/push/MR status, diff stats, error summary, and nullable usage/cost placeholders.
Summarize the ledger:
gah ledger summary --since 7d
gah ledger summary --profile my-repo --since 24hSummary includes backend/mode counts, requested vs effective backend, fallback counts, validation and push rates, MR counts, average duration, and usage/cost totals when known.
Use gah sync --profile my-repo for an explicit current and historical
classification of GAH-created MRs/PRs without dispatching anything new.
Current classifications include:
CI_FAILEDNEEDS_REVIEWNEEDS_FIXREADY_FOR_HUMANMERGEDSTALEUNKNOWN
This pass only prints state and recommended next action. It does not auto-merge or auto-dispatch fix runs.
Remove old GAH-owned sessions and worktrees:
gah prune --dry-run --older-than 14
gah prune --profile my-repo --older-than 30Prune only touches:
artifact_root/sessions/*- worktrees under
defaults.worktree_basewith GAH-owned naming prefixes
On a central node, the same daily gah-prune.timer also runs chat
maintenance. Terminal provider branches settle, and live sessions with no
completed turn for chat_session_idle_days (per profile, default 14) are
archived. Both paths use the normal archive safety behavior: dirty tracked and
untracked work is saved as a patch and the local branch always survives. The
Chat storage card previews per-session usage and the exact reclaim plan before
an operator applies it.
For running and repairing GAH as an unattended service (systemd units, token
scopes, state-file repair commands, notification/manager-wake setup, failure
triage, and the auto-merge safety model), see the operator runbook:
docs/OPERATIONS.md.
gah initgah doctorgah dispatchgah ledger summarygah ledger repair-tail [--dry-run]gah prunegah syncgah profile listgah profile show <name>gah candidatesgah price-guardgah policy-check
Contract tests for the first two exist in tests/gah_cli.rs as #[ignore]d
tests — implement until they pass, then remove the ignore.
gah sync --json: machine-readable MR classification so a manager agent can consume state without parsing pretty-print. Seesync_json_outputs_machine_readable_classification.gah ledger summary --json: same for run history/costs. Seeledger_summary_json_outputs_machine_readable_counts.- Populate ledger usage/cost fields:
usage.*is always null, so the routing cost caps (max_known_estimated_cost_per_weeketc.) can never trigger. Implementation plan (TDD):- Grab 2-3 real
backend-output.logfiles fromartifact_root/sessions/*/attempt-*/(openhands runs with--json, so the log is JSON event lines) and commit trimmed excerpts containing the token/cost fields astests/fixtures/usage-logs/*.log. Do NOT guess the field names — read them from real logs. - Add
pub fn parse_usage_from_log(log_path: &Path, backend: &str) -> LedgerUsageinrunner.rs: scan lines for JSON objects, take the last one containing usage keys (openhands: accumulated cost/token metrics in its event stream; claude: run with--output-format jsonand readtotal_cost_usd/usagefrom the final result object). Unknown format →LedgerUsage::default(), never an error. - Call it after each
run_backendindispatch.rs(improve, pm, review, experiment) and assign toledger.usage; setusage_sourceto the backend name. - Unit tests against the fixtures; assert
gah ledger summarythen shows nonzero cost totals.
- Grab 2-3 real
- Fix strong-run heuristic:
ledger::usage_summary_for_backendcounts every improve/fix/review run as "strong" unlessconfidence_impact == low. Strongness should be determined by model/backend (e.g. a configured strong-model list), not by mode. - Failure taxonomy + attempts in ledger: add to
LedgerEntry:attempts: u32,baseline_validation: Option<String>("passed"/"failed"), andfailure_class: Option<String>with valuesharness_error(validation command could not run / config bug),env_error(baseline failing and failure identical to baseline),agent_no_progress(failure identical across attempts),agent_failure(real failing validation),backend_error(nonzero backend exit). Set these at each bail/success site indispatch::improve. Without this, model-economics stats bill config bugs to the model. - Outcome backfill: a dispatch's real outcome (merged / closed / rotting)
is only known later. Extend
gah syncto join provider MR state back onto ledger entries by branch name and append a{"type":"outcome","branch":...,"state":"merged|closed|open","merged_at":...}record to the ledger (append-only, no rewriting). TDD against the existing fake-ghpattern intests/gah_cli.rs. gah ledger models --since 30d: the economics report. Per (effective_backend, effective_model): dispatches, avg attempts, validation pass rate, harness-vs-agent failure split, MRs opened, MRs merged (from outcome records), total cost, and cost per merged MR — the number that answers "deepseek retries more but is still cheaper than codex". Requires the cost-parsing and outcome-backfill tickets.--jsonoutput included.gah doctor --validate: run the profile'svalidation_commandsin the live repo (read-only) andsh -n-check their syntax, so broken validation config is caught at setup time, not inside a paid dispatch loop. Doctor passing should mean "dispatch will not waste money on config errors".- Smart MR titles from ticket: Parse
Suggested MR Title:field from the ticket file and use it as the MR title instead of the generic[GAH] improve: <repo>. Fall back to generic if field not present.