Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
```
src/
lib.rs — Library root (pub mod re-exports for integration tests; includes webhooks and catch_up modules)
main.rs — CLI entrypoint (initializes tracing subscriber, loads config, loads workflows, validates agents & triggers, starts server; handles `webhooks` subcommand)
main.rs — CLI entrypoint (initializes tracing subscriber, loads config, loads workflows, validates agents & triggers, performs agent health checks, starts server; handles `webhooks` subcommand)
cli.rs — CLI argument parsing (clap derive) with `webhooks` subcommand support
config.rs — Configuration parsing, validation, error types, and env var name constants (env module)
dispatcher.rs — Concurrency control (Dispatcher + Semaphore), deduplication (DedupSets, SharedDedupSets), persistence, workspace directory management, and per-event shallow clone orchestration
harness.rs — Hermes API client harness (HermesClient, request/response types, StepResult, error handling)
harness.rs — Hermes API client harness (HermesClient, request/response types, StepResult, error handling, agent health check with HealthResponse/HealthCheckError/check_agent_health)
file_log.rs — Workflow step audit files: write `.prompt` and `.log` files per agent step (called from runner.rs)
reload.rs — File watcher for workflow hot-reload (notify crate, debouncing, ReloadMessage types, WorkflowState, reload_workflows)
server.rs — axum HTTP server with health, readiness, webhook endpoint, HTTP header constants (headers module), and catch-up invocation before listener start
Expand All @@ -25,11 +25,12 @@ src/
mod.rs — Shared types (TriggerEvent, WebhookError) and dispatch to platform handler
github.rs — GitHub webhook: HMAC-SHA256 verification, event parsing, trigger mapping, and GitHub event type string constants
gitlab.rs — GitLab webhook: token verification, payload parsing, event mapping, and GitLab event type string constants
gitlab_api.rs — GitLab REST API client (GitLabClient, GitLabWebhook, WebhookConfig, ProjectEvent, ProjectPushData, list/create/update/delete_webhooks, list_project_events, find_webhook_by_url, error types)
gitlab_api.rs — GitLab REST API client (GitLabClient, GitLabWebhook, WebhookConfig, ProjectEvent, ProjectPushData, list/create/update/delete webhooks, list_project_events, find_webhook_by_url, error types)
tests/
dispatcher_tests.rs — Integration tests for dispatcher (full dispatch flow, dedup, concurrency, shutdown, persistence)
git_integration_tests.rs — Integration tests for git module (shallow clone, branch sanitization, clone URL building, dirty-check with local repos)
harness_tests.rs — Integration tests for harness (serialization, response parsing, error file, multi-instance)
harness_tests.rs — Integration tests for harness (serialization, response parsing, error file, multi-instance, agent health check)
health_startup_tests.rs — Integration tests for agent health check (multi-agent, connection refused, invalid JSON, URL construction, unhealthy status)
reload_tests.rs — Integration tests for reload module (file detection, debouncing, .toml filtering)
reload_integration_tests.rs — Integration tests for hot-reload (add workflow, invalid file preserves state, file removal)
runner_tests.rs — Integration tests for workflow runner (step execution, template vars, hooks, fail-fast)
Expand All @@ -42,7 +43,7 @@ tests/
- **Canonical `event_id` convention**: Every `TriggerEvent` carries an `event_id` field in canonical form, defined per trigger type in Appendix A of the architecture design. Examples: `issue-42` (GitHub issue assigned or comment mention), `pr-7-review-999` (GitHub PR review), `pr-7-comment-12345` (GitHub PR comment mention), `issue-7` (GitLab issue assigned or mention), `mr-5-review-88` (GitLab MR review), `mr-5-comment-204` (GitLab MR comment mention). Dedup keys, workspace directories, template variables, and log fields all use the canonical form.

- **Library + binary crate**: Yoke is both a library and a binary. `src/lib.rs` re-exports all modules as `pub` so integration tests (`tests/`) can import from `yoke::`. `src/main.rs` uses `use yoke::` imports instead of inline `mod` declarations.
- **Fail-fast on startup**: Invalid config is a hard exit. Errors produce clear messages.
- **Fail-fast on startup**: Invalid config is a hard exit. Errors produce clear messages. Agent health checks are performed after config and workflow validation, before the server starts — if any agent is unreachable or reports an unhealthy status, the process exits with an error.
- **CLI argument parsing**: Uses `clap` with derive macros. `--config` and `--workflows` have defaults; `--host`, `--webhook-host`, and `--port` override values from `config.toml`. `--webhook-host` sets the external hostname used in webhook registration URLs (useful when binding to `0.0.0.0` but advertising a public hostname).
- **Tilde expansion**: `~` in `workdir` is expanded at load time via `shellexpand`.
- **Serde-driven validation**: Required fields are enforced by serde (missing fields = error). Semantic validation (duplicate agents, URL schemes, trigger types, template variables, allowed_users) is done in `Config::validate()` / `Workflow::validate()`.
Expand Down Expand Up @@ -85,6 +86,7 @@ tests/
- **Dedup persistence** (`src/dispatcher.rs`): `FailedEntry` struct records permanently failed events with `{key, timestamp, error}`. `PersistenceError` enum handles IO and JSON errors from file operations. `load_dedup_file` deserializes JSON files (returns `NotFound` for missing, `Json` for corrupted). `save_dedup_file` uses atomic writes — writes to `.json.tmp`, then `rename` to target — to prevent data corruption on crash. `DedupSets::persist_completed` creates the work directory if missing (`create_dir_all`) before saving the `completed` set to `completed.json`. `DedupSets::persist_failed` creates the work directory if missing before appending a `FailedEntry` to `failed.json` (load-append-save pattern; JSON arrays require full rewrite). Error logs include the target file path via a `path` field for diagnostics. `load_persistence` reads `completed.json` and `failed.json` from the work directory at startup, treating missing files as empty sets and logging warnings for corrupted ones. `in_flight` is always empty on load (transient state).
- **Watermark persistence** (`src/dispatcher.rs`): `Watermark` struct records the last-processed delivery/event per repository with `{last_delivery_id: Option<String>, last_event_id: Option<String>, last_processed_at: DateTime<Utc>}`. `WatermarkStore` wraps a `HashMap<String, Watermark>` keyed by `"{owner}/{repo}"` (e.g., `"mintybasil/yoke"`). On successful workflow completion, `handle_dispatch` updates the watermark for the event's repository: `last_delivery_id` is set from `TriggerEvent.delivery_id` (GitHub: the `X-GitHub-Delivery` header UUID; GitLab: `None`), `last_event_id` is set from `TriggerEvent.event_id` (the canonical event identifier), and `last_processed_at` is set to `Utc::now()`. Watermarks are persisted to `watermark.json` in the work directory during `persist_state()` (graceful shutdown) using the same atomic `.tmp` + `rename` pattern as dedup persistence. Empty watermark stores skip file creation. `load_watermarks` reads `watermark.json` at startup, falling back to an empty default for missing or corrupted files. `new_watermark_store` creates an empty `Arc<RwLock<WatermarkStore>>` for use in tests. The `X-GitHub-Delivery` header is extracted in the server's webhook handler and passed as the `delivery_id` parameter to `WebhookHandler::handle_webhook`, which assigns it to `TriggerEvent.delivery_id` before dispatch.
- **Hermes API harness** (`src/harness.rs`): `HermesClient` encapsulates a `reqwest::Client`, `base_url`, and `api_key` for making authenticated POST requests to the Hermes Agent API `/v1/responses` endpoint. `execute_step(instructions, input)` builds a `HermesRequest { instructions, input, store: true }` where `instructions` is `Option<String>` — when `None`, the field is omitted from the JSON payload, sends it via `POST {base_url}/v1/responses` with `Authorization: Bearer *** and parses the response into a `HermesResponse`. Response parsing filters `output` content blocks for `type == "output_text"` and joins their text with newlines. Non-2xx responses write the status code and body to a `.error` file and return `HarnessError::Api`. `HarnessError` has three variants: `Http` (network/request errors — structured with a `message` field that includes the URL, timeout/connect status, and full cause chain via `from_reqwest_error()`), `Api` (non-2xx status with status code and body), and `Io` (file write errors for `.error`). `execute_step_with_error_path` accepts an optional `Path` for the error file (used in tests). `HermesRequest`, `HermesResponse`, and `ContentBlock` derive `Serialize`/`Deserialize` for JSON round-tripping; `ContentBlock` uses `#[serde(rename = "type")]` for the `block_type` field.
- **Agent health check** (`src/harness.rs`): `check_agent_health(agent: &AgentConfig)` is an async function that queries the agent's `/health` endpoint via GET request and verifies the response body contains `status: "ok"`. `HealthResponse` struct (derives `Debug`, `Clone`, `Deserialize`) deserializes the JSON response with `status`, `platform`, and `version` String fields. `HealthCheckError` enum (derives `Debug`, `Error` via thiserror) has three variants: `Http` (network failure with agent name, URL, and message), `BadStatus` (non-200 or non-ok status with agent name, URL, status code, and body), and `Parse` (JSON parse failure with agent name, URL, and error message). The health check is called in `main.rs` `run()` after `validate_triggers` and before `watch::channel(false)` — it iterates over `config.agents`, logs a successful check at `info!` level with agent name, platform, and version, and fails fast with a `ContextError` wrapping `HealthCheckError` if any agent is unreachable or unhealthy. This prevents the server from starting when agent connectivity is broken.
- **StepResult struct** (`src/harness.rs`): `StepResult` captures the output of a single agent step execution. Fields: `extracted_message` (the text from `output_text` content blocks), `raw_request` (the full JSON request body sent to the API), and `raw_response` (the full JSON response body received). Both `execute_step` and `execute_step_with_error_path` return `Result<StepResult, HarnessError>` instead of `Result<String, HarnessError>`, enabling audit logging of the full HTTP exchange per step.
- **Workflow step audit files** (`src/file_log.rs`, called from `src/runner.rs`): `write_prompt_file(step_num, step_name, prompt, workspace_dir)` writes a rendered prompt template to `{workspace_dir}/{step_num:02}_{step_name}.prompt` *before* the API call. `write_request_log_file(step_num, step_name, request, workspace_dir)` writes just the request portion to `{workspace_dir}/{step_num:02}_{step_name}.log` *before* the API call, ensuring the request is always logged even if the API call fails. `write_log_file(step_num, step_name, request, response, extracted_message, workspace_dir)` overwrites the log file with the full HTTP exchange *after* a successful API call, with sections for `REQUEST:`, `RESPONSE:`, and `FINAL MESSAGE:`. All three functions create the workspace directory if it doesn't exist. `HermesClient::build_request_body(instructions, input)` serializes the request body for pre-call logging without sending it. File naming uses zero-padded two-digit step numbers (e.g., `00_Plan.log`, `01_Analyze.prompt`). File writing is performed in `WorkflowRunner::execute_step()` using an `AtomicUsize` step counter for zero-based numbering.
- **Workspace directory** (`src/dispatcher.rs`): `workspace_dir(workdir, owner, repo, event_id)` constructs the per-event workspace path `{workdir}/{owner}/{repo}/{event_id}/` per the architecture design doc (Section 11). The dispatcher creates this directory before spawning the workflow task. Git orchestration is **config-driven**: when any matching workflow has `[git] clone = true`, the dispatcher performs a per-event shallow clone (`git clone --depth=1 --branch <branch>`) directly into the workspace directory via `git::shallow_clone()`. The clone URL has the auth token embedded via `embed_token_in_url()` for CLI-based authentication. The `worktree` feature has been removed — `GitConfig` now rejects `worktree = true` at deserialization time with a clear migration error. Branch resolution: the dispatcher prefers `TriggerEvent.branch` (from the webhook payload), falling back to the workflow's `git.default_branch`. The `clone` field defaults to `false` (opt-in). The actual prompt and log files are written by `WorkflowRunner::execute_step()` using real API request/response data.
Expand Down Expand Up @@ -192,6 +194,11 @@ Dispatcher tests cover:
- Semaphore stress: high-concurrency with bounded permits

The `tests/webhooks_tests.rs` file contains integration tests for the webhooks CLI command handlers. These tests use `mockito` to mock the GitHub API and verify the behavior of `webhooks_list`, `webhooks_remove`, and `webhooks_add` against various scenarios (empty repos, API errors, existing hooks, new hooks, URL matching).

The `tests/harness_tests.rs` file contains integration tests for the Hermes API client harness, including serialization, response parsing, error file format, and agent health check tests (healthy agent, bad status, HTTP error, parse error, unhealthy status, trailing slash URL).

The `tests/health_startup_tests.rs` file contains integration tests for the agent health check feature, covering multi-agent scenarios (all healthy, one unhealthy), connection refused, invalid JSON, non-ok status, and URL construction verification.

## Logging

Note: This section covers application-level structured logging (console/stderr output via the `tracing` crate). This is distinct from the workflow step audit files written by `src/file_log.rs`, which record per-step HTTP exchanges to disk.
Expand Down Expand Up @@ -280,4 +287,4 @@ RUST_LOG=yoke=warn cargo run

# Trace level for intense debugging
RUST_LOG=yoke=trace cargo run
```
```
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ cargo run -- --config /path/to/config.toml --workflows /path/to/workflows

Yoke listens for webhook events on `http://{host}:{port}/webhook`. The `webhook_host` setting determines the hostname used in webhook registration URLs, which may differ from the bind address (`host`), for example, binding to `0.0.0.0` locally while advertising `yoke.example.com` in webhook URLs.

### 6. Agent Health Check

On startup, Yoke performs a connectivity check for all configured agents by querying each agent's `/health` endpoint. The agent is expected to return a JSON response like:

```json
{"status": "ok", "platform": "hermes-agent", "version": "0.17.0"}
```

If `status` is `ok` for all agents, Yoke proceeds with startup. If any agent is unreachable, returns a non-200 status code, or reports a status other than `ok`, Yoke reports an error and exits without starting the server. This helps surface connectivity issues early before any webhook events are processed.

## Configuration

Yoke reads configuration from a `config.toml` file. The default path is `config.toml` in the current directory; override with `--config`.
Expand Down Expand Up @@ -301,7 +311,7 @@ Creates or updates webhooks on all configured repositories, subscribing to the e
yoke --config config.toml webhooks remove
```

Removes all Yoke webhooks (matched by URL) from each configured repository.
Removes all Yoke webhooks (matched by URL) from each repository.

## Secure Webhook Exposure with Tailscale Funnel

Expand Down
Loading
Loading