Skip to content

Repository files navigation

git-agent-harness

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.

Binary releases (no build required)

Pre-built binaries are published to GitHub Releases. A GitHub token with repo read scope is required because the repository is private.

Windows — desktop worker (NSIS installer)

$env:GITHUB_TOKEN="ghp_xxxxxxxxxxxx"
irm "https://raw.githubusercontent.com/Kh1ng/git-agent-harness/main/scripts/install-windows.ps1" | iex

Smoke test: GAH Worker appears in the system tray after install.

Linux — desktop worker (AppImage)

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.AppImage

Smoke test: GAH Worker appears in the system tray.

Android — control surface (debug APK)

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.

CLI binary (gah) — Linux or macOS

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/gah

Smoke test:

gah --help
gah doctor

Requirements

  • git
  • Rust toolchain (cargo and rustup)
  • One backend CLI:
    • codex
    • claude
    • openhands
  • Provider tooling:
    • GitHub: gh
    • GitLab: glab for provider API access, plus token env vars for pushes

Install

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.toml

scripts/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.sh

This 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-server

cargo 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.

Manager chat mock development

Run the real Vite frontend against the stateful, in-memory mock control plane:

npm ci --include=dev
npm run dev:mock

Open 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/reset

You can also choose the initial scenario when starting development:

npm run dev:mock -- --scenario models-agy

Named 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.toml
  • config/gah-config.gitlab.example.toml
  • config/gah-config.gitlab-self-hosted.example.toml

What GAH Creates

  • 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.

Config Basics

GAH loads config from:

  1. --config
  2. GAH_CONFIG
  3. ~/.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 = 3600

GitLab adds:

provider_api_base = "https://gitlab.example.com/api/v4"
provider_project_id = "12345"

Secrets do not go in config.

Routing precedence:

  1. explicit CLI backend/model override
  2. profile routing config
  3. global defaults.routing
  4. 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 = 3

Unless 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.

Subscription routing setup

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 = 100

Repeat 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 --json

Paid 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 = true

When 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-fallback

The 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.

Auth

  • GitHub: set GITHUB_TOKEN or GH_TOKEN
  • GitLab: set GITLAB_PAT or GITLAB_PAT2
  • LLM proxy: set LLM_API_KEY if needed

GAH keeps push auth in askpass; it does not embed tokens into remotes or push URLs.

Setup

GitHub

gh auth login
export GITHUB_TOKEN=...
gah doctor --profile my-repo

GitLab.com

glab auth login --hostname gitlab.com
export GITLAB_PAT=...
gah doctor --profile my-repo

Self-Hosted GitLab

glab 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.

Onboarding

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 12345

Preview without writing:

gah init ... --print

Doctor

Check config and profile readiness:

gah doctor --profile my-repo
gah doctor

Doctor 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.md exists
  • generated-artifact publication patterns are valid

First Dispatch

Trusted issue intake

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.

Generated-artifact publication guard

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-run

Then 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 pm

PM 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.json

PM Mode

PM 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 = 900

pm_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.

Retries

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.

Experiment Mode

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.

Env Files

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.

Manager Agent

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.

Project skill bindings

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 Gate

Review mode now produces:

  • review-report.md
  • review-verdict.json

Verdicts are:

  • APPROVE_STRONG
  • APPROVE_WEAK
  • NEEDS_FIX
  • REJECT
  • HUMAN_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-human
  • gah-needs-fix
  • gah-human-review
  • gah-review-weak

Ledger

Inspect recent runs:

tail -n 20 ~/.config/gah/ledger.jsonl
jq . ~/.config/gah/ledger.jsonl | less

Fields 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 24h

Summary includes backend/mode counts, requested vs effective backend, fallback counts, validation and push rates, MR counts, average duration, and usage/cost totals when known.

Sync

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_FAILED
  • NEEDS_REVIEW
  • NEEDS_FIX
  • READY_FOR_HUMAN
  • MERGED
  • STALE
  • UNKNOWN

This pass only prints state and recommended next action. It does not auto-merge or auto-dispatch fix runs.

Prune

Remove old GAH-owned sessions and worktrees:

gah prune --dry-run --older-than 14
gah prune --profile my-repo --older-than 30

Prune only touches:

  • artifact_root/sessions/*
  • worktrees under defaults.worktree_base with 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.

Operating GAH Unattended

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.

Command Summary

  • gah init
  • gah doctor
  • gah dispatch
  • gah ledger summary
  • gah ledger repair-tail [--dry-run]
  • gah prune
  • gah sync
  • gah profile list
  • gah profile show <name>
  • gah candidates
  • gah price-guard
  • gah policy-check

TODO / Backlog

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. See sync_json_outputs_machine_readable_classification.
  • gah ledger summary --json: same for run history/costs. See ledger_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_week etc.) can never trigger. Implementation plan (TDD):
    1. Grab 2-3 real backend-output.log files from artifact_root/sessions/*/attempt-*/ (openhands runs with --json, so the log is JSON event lines) and commit trimmed excerpts containing the token/cost fields as tests/fixtures/usage-logs/*.log. Do NOT guess the field names — read them from real logs.
    2. Add pub fn parse_usage_from_log(log_path: &Path, backend: &str) -> LedgerUsage in runner.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 json and read total_cost_usd / usage from the final result object). Unknown format → LedgerUsage::default(), never an error.
    3. Call it after each run_backend in dispatch.rs (improve, pm, review, experiment) and assign to ledger.usage; set usage_source to the backend name.
    4. Unit tests against the fixtures; assert gah ledger summary then shows nonzero cost totals.
  • Fix strong-run heuristic: ledger::usage_summary_for_backend counts every improve/fix/review run as "strong" unless confidence_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"), and failure_class: Option<String> with values harness_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 in dispatch::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 sync to 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-gh pattern in tests/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. --json output included.
  • gah doctor --validate: run the profile's validation_commands in the live repo (read-only) and sh -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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages