# hu-agent

Autonomous AI agent that monitors Jira for bug tickets and bot-authored GitHub pull requests, runs the Cursor CLI agent with repo-specific rules, validates changes locally, and either opens or updates GitHub PRs with minimal human intervention.

## Current Status

The project is currently operating as a dual-trigger automation service with two first-class workflows:

- **Jira fix pipeline**: triages assigned Jira tickets, fixes code across one or more repos, validates changes, and opens PRs.
- **PR comment pipeline**: attends follow-up feedback on bot-authored PRs, reads prior conversation context, downloads comment screenshots, replies in-thread, and pushes follow-up fixes to the same branch when needed.

Implemented capabilities worth relying on today:

- Read-only triage before attempting Jira fixes
- Multi-repo handling with per-repo branching, validation, and Jira subtasks
- Repo-aware rules loading from bot-side rules, repo-owned rules, and remote rules repos
- PR label validation against existing repo labels before fallback defaults are applied
- Optional Slack notifications for ticket start, PR creation, `NEEDS_INFO`, and PR comment attendance
- Persistent per-PR memory stored locally so follow-up comment handling can reuse earlier context

Operationally, the service currently manages 10 repositories declared in [`repos.json`](./repos.json), runs both pollers from a single process, and processes jobs through a single in-memory queue.

## How It Works

```
Jira Poller (every 5s)
  │  Finds tickets in "To Do" assigned to bot
  ▼
In-memory Job Queue (concurrency: 1, max retries: 2, 35min timeout)
  │
  ▼
Fix Pipeline
  ├─ 1.  Transition issue → "In Progress"
  ├─ 2.  Fetch full issue (description, parent, attachments, comments)
  ├─ 3.  Sync all repos (clone/pull + create fix branch) — parallel
  ├─ 4.  Sync remote rules repos (clone/pull + symlink .mdc files)
  ├─ 5.  Download Jira image attachments (max 5 images, max 5 MB each)
  ├─ 6.  Load repo-specific rules (index files + linked .mdc files)
  ├─ 7.  Phase 1 — Triage: Cursor CLI assesses if ticket is actionable
  │       └─ NEEDS_INFO → post Jira comment, revert to "To Do", stop
  │       └─ PROCEED → continue
  ├─ 8.  Phase 2 — Fix: Cursor CLI fixes code across all repos (30min timeout)
  ├─ 9.  Detect which repos have changes
  ├─ 10. For each changed repo:
  │       ├─ If multiple repos changed → create Jira subtask per repo
  │       ├─ Commit & push (lock files excluded automatically)
  │       ├─ Validate build (run checkScripts, skip pre-existing errors)
  │       ├─ On build failure → followup agent + retry once
  │       ├─ Parse PR title/body/labels from agent output markers
  │       └─ Create GitHub PR + apply validated labels
  └─ 11. Cleanup temp files

PR Comment Poller (every 5s)
  │  Finds new comments on bot-authored PRs
  ▼
PrComment Pipeline
  ├─ 1.  Checkout PR branch and sync repo rules
  ├─ 2.  Fetch PR diff + previous PR conversation from GitHub
  ├─ 3.  Load per-PR memory from memory/<repo>#<pr>.json
  ├─ 4.  Download screenshots embedded in the comment into .hu-bot/
  ├─ 5.  Cursor CLI reads current comment + previous context + images
  ├─ 6.  Parse reply from REPLY_START/REPLY_END markers
  ├─ 7.  Post reply (inline review reply or issue comment)
  ├─ 8.  Persist attended interaction to local PR memory
  └─ 9.  Commit & push any code changes made
```

## Project Structure

```
src/
├── index.ts                  Entry point — DI wiring, server start, graceful shutdown
├── core/
│   ├── constants.ts          Safety limits, timeouts, queue config, scoring weights, memory limits
│   └── errors.ts             Domain error classes (JobTimeoutError, etc.)
├── utils/
│   ├── config.ts             Zod-validated env config loader
│   ├── logger.ts             Pino logger factory
│   ├── slug.ts               Branch name slugifier (max 48 chars)
│   ├── package-manager.ts    Detects npm/yarn/pnpm/bun per repo
│   └── node-version.ts       Resolves .nvmrc / .node-version per repo
├── jira/
│   ├── client.ts             JiraClientImpl — REST API v3, ADF→Markdown, subtask creation
│   └── schemas.ts            Zod schemas for Jira payloads
├── github/
│   ├── client.ts             GitHubClientImpl — Octokit wrapper (PRs, labels, comments)
│   └── auth.ts               PAT or GitHub App token provider (auto-refresh)
├── cursor/
│   ├── cli-client.ts         CursorCliClient — spawns `cursor agent` CLI process
│   ├── cloud-client.ts       CursorCloudClient — HTTP client (debug API only)
│   ├── prompts.ts            Prompt builders + output parsers (title, body, labels, triage)
│   └── client.ts             Shared types
├── memory/
│   └── pr-memory.ts          File-backed per-PR memory store for attended comments
├── repo/
│   ├── manager.ts            RepoManagerImpl — git ops, build validation, node version
│   ├── registry.ts           Loads and validates repos.json (Zod schema)
│   ├── selector.ts           Keyword scoring to pick the right repo for a ticket
│   └── rules-sync.ts         Clones remote rules repos and symlinks .mdc files
├── queue/
│   └── memory-queue.ts       In-memory FIFO queue with retry, timeout, job-kind awareness
├── poller/
│   ├── jira-poller.ts        Polls Jira every 5s for assigned "To Do" tickets
│   └── pr-comment-poller.ts  Polls GitHub every 5s for new comments on bot PRs
├── pipeline/
│   ├── fix-pipeline.ts       Main Jira → PR orchestration
│   ├── pr-comment-pipeline.ts  PR comment → reply + code change + memory/image context
│   └── safety.ts             Diff safety validation utility
├── slack/
│   └── client.ts             Optional Slack notifications for lifecycle events
├── datadog/
│   └── client.ts             Datadog Logs API client (debug endpoint)
└── server/
    ├── app.ts                Hono app factory
    └── routes/
        ├── health.ts         GET /health, GET /metrics
        ├── webhook.ts        POST /jira/webhook (legacy, still supported)
        └── debug.ts          Full debug/admin REST API
```

## Cursor Agent CLI

The bot spawns the Cursor agent via CLI. There are two binaries involved:

- **`/Applications/Cursor.app/Contents/Resources/app/bin/cursor`** — A bash wrapper script that ships with the Cursor desktop app. When invoked with `agent`, it delegates to `cursor-agent` but first runs install/version-check/update steps that can hang in headless environments.
- **`~/.local/bin/cursor-agent`** — The actual agent binary, installed via `curl -sS https://cursor.com/install | bash`.

The bot calls `cursor-agent` directly (bypassing the bash wrapper) to avoid the version-check hang. It falls back to the `cursor` wrapper only if `~/.local/bin/cursor-agent` is not found.

Make sure `cursor-agent` is installed before running:

```bash
curl -sS https://cursor.com/install | bash
```

## Prerequisites

- [Bun](https://bun.sh) >= 1.1
- `cursor-agent` installed (`curl -sS https://cursor.com/install | bash`)
- `git` installed locally
- SSH key configured with access to all target GitHub repositories
- Cursor API key — [cursor.com/dashboard](https://cursor.com/dashboard)
- GitHub PAT (or GitHub App credentials) with `repo` scope
- Jira Cloud API token

## Setup

```bash
git clone <repo-url>
cd hu-agent
bun install

cp .env.example .env
# Edit .env with your credentials

bun run dev    # watch mode
bun run start  # production
```

## Developer tutorial

Hands-on steps for configuring credentials, running the service, triggering a fix job, calling the HTTP API from Insomnia, and reading production logs. For the full variable list, see [Environment Variables](#environment-variables). For every route (including Pipeline and Datadog), see [API Endpoints](#api-endpoints). For how the Jira → fix → PR flow behaves, see [docs/jira-fix-pipeline.md](./docs/jira-fix-pipeline.md).

### Environment variables (team reference)

- Internal env documentation (secrets and prod values): [Google Drive folder](https://drive.google.com/drive/folders/1Y3S-j7GjV7R8Pus3HahTG2zRYnxFqerB?usp=sharing) (requires org access).
- The repo ships [`.env.example`](./.env.example) as the checked-in template; copy it to `.env` and fill in values. Do not commit `.env`.

### Create a Jira Cloud API token

Use the **same Atlassian account** as `JIRA_EMAIL` (typically the bot or service user the bot uses).

1. Sign in at [id.atlassian.com](https://id.atlassian.com).
2. Open **Security** → **Create and manage API tokens**.
3. Create a token, copy it once, and set `JIRA_API_TOKEN` in `.env`.

Official reference: [Manage API tokens for your Atlassian account](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/).

### Run the service locally

Follow [Setup](#setup) (`bun install`, `cp .env.example .env`, edit `.env`, `bun run dev`). Confirm the process is up with `GET /hu-agent/health` (default port `3000`).

### Trigger a fix pipeline (manual ticket)

Enqueue a full fix run for an existing Jira issue:

```bash
curl -sS -X POST "http://localhost:3000/hu-agent/api/pipeline/fix" \
  -H "Content-Type: application/json" \
  -d '{"issueKey":"SQDP-123"}'
```

A successful enqueue returns **202** with `{ "accepted": true, "issueKey": "..." }`. `POST /hu-agent/api/pipeline/trigger` with the same JSON body behaves the same today. The job runs asynchronously on the in-memory queue; the server must have valid Jira, GitHub, and Cursor configuration.

### Insomnia: explore the HTTP API

Import the bundled Insomnia export [`docs/api-docs/collection.yaml`](./docs/api-docs/collection.yaml) (Insomnia: import → OpenAPI / Insomnia format). The workspace includes **Base** plus **Local** and **Prod** sub-environments: `base_url` already includes the `/hu-agent` prefix (e.g. `http://localhost:3000/hu-agent` for Local). Use the **Pipeline** group for **Pipeline Fix** / **Pipeline Trigger** requests. Requests to `localhost` only reach a server running on your machine; to hit the deployed API, switch to the **Prod** environment (or use a tunnel to your own instance).

### Fetch production logs (Datadog)

`GET /hu-agent/api/datadog/logs` searches logs via the Datadog Logs API. Query parameters include `query`, `window` (`15m` or `24h`), optional ISO `from` / `to`, and `limit` (see the **Datadog** row in [API Endpoints](#api-endpoints)). The **instance you call** must have `DATADOG_API_KEY`, `DATADOG_APP_KEY`, and `DATADOG_SITE` set (values often come from the Drive doc for production).

Example (local process with Datadog keys in `.env`):

```bash
curl -sS "http://localhost:3000/hu-agent/api/datadog/logs?window=24h&limit=500"
```

## What Is Implemented Now

Recent capabilities that are implemented in code and active in the current architecture:

- **PR comment images**: screenshots embedded in GitHub comments are resolved via rendered comment HTML, downloaded into `.hu-bot/`, and made available to the agent during follow-up runs.
- **Persistent PR memory**: attended PR comment interactions are stored locally in `memory/` and injected into later follow-up prompts for the same PR.
- **Label validation**: agent-suggested labels are checked against existing repo labels before they are applied; if they do not match, the pipeline falls back to repo-configured defaults.
- **Slack notifications**: optional messages are sent for ticket start, PR creation, `NEEDS_INFO`, and PR comment attendance.
- **Remote rules sync**: shared rules repos can be cloned and symlinked into managed repos so prompt rules stay current without copying files around.
- **Custom metrics**: the bot emits `hu_agent.*` DogStatsD metrics (job lifecycle, queue depth, poll activity) to Datadog us5 via hot-shots. An `AuthChecker` runs periodic per-integration auth probes (jira/slack/github/agent) and reports credential health as a gauge, decoupled from the job queue so failures surface even when the queue is wedged. Pipeline usage metrics (`repo.selected`, `pr.opened`, `fix.outcome`, `agent.invocations/duration`, `pr_comment.handled`, `slack_mention.handled`, and more) are emitted across the fix, pr-comment, and slack pipelines, enabling an agent-usage dashboard. Dashboards and monitors are built manually in Datadog.

## Environment Variables

| Variable | Required | Default | Description |
|---|---|---|---|
| `JIRA_EMAIL` | yes | — | Bot's Jira Cloud email (must match the account that created `JIRA_API_TOKEN`); `accountId` from `GET /myself`, fallback user search |
| `JIRA_API_TOKEN` | yes | — | Jira Cloud API token |
| `JIRA_DOMAIN` | yes | — | Jira subdomain (`humand` for humand.atlassian.net) |
| `JIRA_POLL_ENABLED` | no | `"true"` | Toggle the Jira poller |
| `JIRA_FLOW_FIELD_ID` | no | `customfield_10200` | Custom field ID for flow type (hotfix/bugfix/regular) |
| `GITHUB_TOKEN` | one of | — | GitHub personal access token |
| `GITHUB_APP_ID` | one of | — | GitHub App ID (alternative to PAT) |
| `GITHUB_APP_INSTALLATION_ID` | one of | — | GitHub App Installation ID |
| `GITHUB_APP_PRIVATE_KEY` | one of | — | GitHub App private key PEM (`\n` literals accepted) |
| `GITHUB_BOT_USER` | no | `hu-agent[bot]` | GitHub login used to identify bot-authored PRs |
| `CURSOR_API_KEY` | yes | — | Cursor API key for CLI auth |
| `CURSOR_MODEL` | no | `auto` | Model name passed to `cursor agent --model` |
| `WORKDIR_PATH` | no | `./workdir` | Directory where repos are cloned |
| `PORT` | no | `3000` | HTTP server port |
| `LOG_LEVEL` | no | `info` | Pino log level |
| `DATADOG_API_KEY` | no | — | Datadog API key (required with `DATADOG_APP_KEY` for logs debug API) |
| `DATADOG_APP_KEY` | no | — | Datadog Application key |
| `DATADOG_SITE` | no | `us5.datadoghq.com` | Datadog site host (`https://api.<site>` for API calls) |
| `DD_METRICS_ENABLED` | no | `true` in production, `false` otherwise | Enable DogStatsD custom metrics (`hu_agent.*`) |
| `DD_AGENT_HOST` | no | `localhost` | Host where the Datadog Agent is running (DogStatsD target) |
| `DD_DOGSTATSD_PORT` | no | `8125` | UDP port of the Datadog Agent DogStatsD listener |

GitHub auth supports either a PAT (`GITHUB_TOKEN`) or a GitHub App (`GITHUB_APP_ID` + `GITHUB_APP_INSTALLATION_ID` + `GITHUB_APP_PRIVATE_KEY`). App tokens are cached and auto-refreshed 5 minutes before expiry.

## Repository Configuration (`repos.json`)

All managed repositories are declared in `repos.json` at the project root:

```json
{
  "repos": [
    {
      "name": "my-api",
      "url": "git@github.com:org/my-api.git",
      "description": "Main backend API. TypeScript, REST, PostgreSQL.",
      "keywords": ["api", "backend", "server", "endpoint"],
      "defaultBranch": "develop",
      "subtaskPrefix": "Backend",
      "rulesFile": "rules/my-api/hu-developer-bot.mdc",
      "checkScripts": ["build", "lint"],
      "prTitleTemplate": "[{key}] | {type} | {summary}",
      "prLabels": {
        "bugfix": ["bugfix"],
        "hotfix": ["bugfix", "hotfix"],
        "default": ["Tech"]
      }
    }
  ]
}
```

### Field Reference

| Field | Required | Description |
|---|---|---|
| `name` | yes | Identifier; must match the directory name inside `workdir/` |
| `url` | yes | Git SSH URL for cloning |
| `description` | yes | Plain-text description injected into every agent prompt; also drives keyword scoring |
| `keywords` | yes | Matched against the Jira ticket to select this repo for a fix |
| `defaultBranch` | no | Base branch (`develop` by default; `main` for some repos) |
| `subtaskPrefix` | no | Prefix for Jira subtask titles when multiple repos are changed (e.g. `"Backend"`, `"Mobile"`) |
| `rulesIndexFile` | no | Path inside the cloned repo to the team's rules index file (default: `.cursor/rules/hu-agent.mdc`) |
| `rulesFile` | no | Fallback path relative to the bot CWD pointing to a bot-side rules index |
| `remoteRulesRepo` | no | SSH URL of an external rules-only repo; its `.mdc` files are symlinked into the cloned repo's `.cursor/rules/` |
| `checkScripts` | no | `package.json` script names run to validate the fix before creating the PR |
| `prTitleTemplate` | no | Template for PR titles; supports `{key}`, `{summary}`, `{type}` placeholders |
| `prLabels` | no | GitHub labels per flow type: `bugfix`, `hotfix`, `default` (fallback for regular flow) |

### Repo Selection

When a Jira ticket arrives, the system scores every repo in `repos.json` against the ticket text (summary + description + stacktrace):
- Each keyword match → +2 points
- Each word in description that matches → +1 point

The repo with the highest score above `MIN_SCORE_THRESHOLD = 1` is selected. If no repo scores above the threshold, the job is aborted and logged.

## Agent Rules & Guidelines

The bot injects repo-specific rules into every agent prompt so Cursor understands each team's coding conventions, PR format, linting requirements, etc.

### Rules Hierarchy

There are three layers of rules, loaded in order and concatenated into the agent prompt:

```
1. rules/general.mdc                      — global rules, always injected
        ↓
2. rules/<repo>/                          — bot-side repo rules (fallback, managed here)
        ↓
3. workdir/<repo>/.cursor/rules/          — team-owned repo rules (preferred, lives in the repo)
```

**Layer 1 — `rules/general.mdc`** (global)
Contains only truly universal instructions: language, code quality, pre-PR validation steps, and the output protocol (PR title/body/labels markers). It does **not** contain any repo-specific logic. It delegates all repo-specific decisions (title format, label logic, PR body structure) to the repo's own template.

**Layer 2 — `rules/<repo>/`** (bot-side, this repo)
Contains repo-specific rules managed by the bot team. Used as a fallback when a repo doesn't have its own `.cursor/rules/` setup yet, or to supplement team-owned rules. Example: `rules/humand-main-api/pr_template.mdc` defines the PR title format and label mapping for that repo.

**Layer 3 — `workdir/<repo>/.cursor/rules/`** (team-owned, lives in the target repo)
The preferred location for repo-specific rules. Each team maintains their own guidelines directly in their repository — coding conventions, PR templates, module structure, state management patterns, etc. The bot loads these automatically by following markdown links in the repo's `hu-agent.mdc` index file (or legacy `hu-developer-bot.mdc`).

Both layer 2 and layer 3 are loaded and concatenated. If a repo has both, the team-owned rules (layer 3) complement rather than replace the bot-side rules (layer 2).

> **Key principle**: `general.mdc` says *how* to output (markers, format). Each repo's template says *what* to output (title format, which labels, PR body structure). This keeps global rules generic and each repo fully in control of its own conventions.

### Index Files

Both layer 2 and layer 3 work as **index files**: they contain relative markdown links to the actual guideline documents. The pipeline parses those links, loads each referenced file, and concatenates everything.

Example `hu-agent.mdc`:
```markdown
[PR Template](./.cursor/rules/pull-request-template.mdc)
[Module Structure](./.cursor/rules/module-structure.mdc)
[State Management](./.cursor/rules/state-management.mdc)
```

### Remote Rules Repos

Some repos share a common rules repository (e.g. `web-cursor-rules`). The bot:
1. Clones/pulls the remote rules repo into `workdir/<rules-repo-name>/`
2. Finds all `.mdc` files in `rules/` subdirectory
3. **Symlinks** each file into `workdir/<code-repo>/.cursor/rules/`

This way, every time the rules repo is updated, all repos sharing it automatically pick up the latest guidelines.

### Size Limits

- Each linked file is capped at **8,000 characters**
- Total rules per repo capped at **32,000 characters**
- HTTP/HTTPS links in index files are ignored; only relative file paths are loaded

## Agent Output Protocol

The agent communicates structured outputs via text markers that the pipeline parses:

| Marker | Content | Fallback |
|---|---|---|
| `PR_TITLE_START` / `PR_TITLE_END` | PR title string | `buildPrTitle()` using `prTitleTemplate` |
| `PR_BODY_START` / `PR_BODY_END` | Full PR body markdown | `buildPrBody()` generic template |
| ~~`PR_LABELS_START` / `PR_LABELS_END`~~ | _(removed — labels are resolved by pipeline)_ | `resolveLabels()` in `label-resolver.ts` |
| `REPLY_START` / `REPLY_END` | Text reply for a PR comment | Full raw agent output (truncated to 4,000 chars) |
| `VERDICT: PROCEED/NEEDS_INFO` | Triage decision | Defaults to `PROCEED` on parse failure |

## Safety Rules

Before creating a PR, the pipeline enforces:

| Rule | Limit |
|---|---|
| Max lines changed | 500 |
| Max files changed | 10 |
| Max files deleted | 3 |
| CI/CD files | Never modified (`.github/workflows/`, `.gitlab-ci.yml`, etc.) |
| Lock files | Never committed (`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `bun.lock`) |

The build validation step is smart about pre-existing errors: if `check-types` or `lint` fails but the errors are **not in the files changed by the agent**, the failure is skipped and the PR is created anyway.

## Triage Phase

Before attempting any fix, the pipeline runs a **read-only triage pass** with a 16-minute timeout. The triage agent:
1. Reads the ticket (description, parent, acceptance criteria, stacktrace)
2. Navigates the relevant repository to understand the code area
3. Decides whether the ticket has enough information to be fixed

**`NEEDS_INFO`**: the bot posts a Jira comment listing what's missing and moves the ticket back to "To Do". No code is touched.

**`PROCEED`**: the triage summary is injected into the fix prompt as pre-computed context, saving the fix agent redundant investigation.

## Multi-Repo Changes & Subtasks

If the fix agent touches more than one repository, the pipeline:
1. Creates a **Jira subtask per repo** under the original issue (with the repo's `subtaskPrefix`)
2. Creates a separate branch and PR per repo, each linked to its subtask key
3. Replaces all references to the original issue key in the agent output with the subtask key before generating the PR body

## PR Comment Response

The bot monitors all open PRs it has authored (checked every 5 seconds when enabled). When a new comment is found:
- The bot fetches the latest comment, prior PR conversation, and PR diff for context
- Loads local per-PR memory from `memory/<repo>#<pr>.json`
- Downloads screenshots embedded in the comment into `.hu-bot/` when present
- Spawns the Cursor agent to draft a response and optionally apply code changes
- Posts the reply in-thread (inline review reply or conversation comment)
- Stores the attended interaction and changed files in local PR memory
- Commits and pushes any code changes to the existing PR branch

The poller skips comments made by the bot itself to avoid infinite loops and only attends bot-authored PRs.

## API Endpoints

### Core

| Method | Path | Description |
|---|---|---|
| `GET` | `/health` | `{ status: "ok", uptime: <ms> }` |
| `GET` | `/metrics` | Uptime, timestamp, memory usage |
| `POST` | `/jira/webhook` | Receives Jira webhook events (legacy trigger, still supported) |

### Debug / Admin

All endpoints under `/api/` — useful for development and manual operations.

**Jira**

| Method | Path | Description |
|---|---|---|
| `GET` | `/api/jira/issue?issueKey=` | Fetch full parsed Jira issue |
| `GET` | `/api/jira/search` | Search issues with query params (`project`, `status`, `assignee`, `text`, `jql`, etc.) |
| `GET` | `/api/jira/myself` | Fetch the authenticated Jira user |
| `GET` | `/api/jira/user?email=` | Look up Jira user by email |
| `GET` | `/api/jira/transitions?issueKey=` | List available status transitions |
| `POST` | `/api/jira/comment` | Add comment to an issue |
| `POST` | `/api/jira/transition` | Execute a status transition |
| `POST` | `/api/jira/subtask` | Create a subtask |

**Repos**

| Method | Path | Description |
|---|---|---|
| `GET` | `/api/repos` | List all configured repos |
| `POST` | `/api/repos/match` | Score repos against `{ summary, description }` |
| `POST` | `/api/repos/check` | Run `validateBuild()` on a repo |

**GitHub**

| Method | Path | Description |
|---|---|---|
| `GET` | `/api/github/pulls?repo=&branch=` | List open PRs or fetch the PR for a branch |
| `GET` | `/api/github/pulls/diff?repo=&pr=` | Get PR unified diff |
| `GET` | `/api/github/pulls/comments?repo=&pr=` | List conversation comments |
| `GET` | `/api/github/pulls/review-comments?repo=&pr=` | List inline review comments |
| `GET` | `/api/github/pulls/review-comment?repo=&commentId=` | Get a single review comment |
| `GET` | `/api/github/pulls/reviews?repo=&pr=&includeComments=true` | List PR reviews and optionally inline comments per review |
| `POST` | `/api/github/pulls` | Create a PR manually for `{ repo, branch, title, body, base }` |

**Pipeline**

| Method | Path | Description |
|---|---|---|
| `POST` | `/api/pipeline/triage` | Run only the triage phase for `{ issueKey }` |
| `POST` | `/api/pipeline/fix` | Enqueue a full fix pipeline for `{ issueKey }` |
| `POST` | `/api/pipeline/trigger` | Enqueue a full fix pipeline for `{ issueKey }` (202 Accepted) |

**Cursor**

| Method | Path | Description |
|---|---|---|
| `GET` | `/api/cursor/models` | List available Cursor models |
| `GET` | `/api/cursor/agents?id=` | Get agent run details |
| `GET` | `/api/cursor/agents/conversation?id=` | Get agent conversation messages |

**Datadog**

| Method | Path | Description |
|---|---|---|
| `GET` | `/api/datadog/logs` | Search logs via Datadog Logs API (`query`, `window` = `15m` or `24h`, optional `from`/`to` ISO range max 24h, `limit` up to 50_000). `window=24h` covers the last 24 hours and paginates Datadog results (default `limit` 10_000 unless set). Response: `{ logs, from, to, total, truncated? }` — each line is `ISO8601` + Pino `msg` (plus optional `issueKey` / `jobKey` / `repo` / `branch`), ANSI stripped; empty lines omitted. |

All routes above are mounted under the `/hu-agent` prefix (e.g. `GET /hu-agent/api/datadog/logs`).

**Verification:** `bun run check` (typecheck + lint + format). With keys set, e.g. `curl -sS "http://localhost:3000/hu-agent/api/datadog/logs?window=24h&limit=500"`.

## Key Internal Limits

| Constant | Value | Description |
|---|---|---|
| `QUEUE.CONCURRENCY` | 1 | One job at a time |
| `QUEUE.MAX_RETRIES` | 2 | Retry failed jobs up to 2 times |
| `QUEUE.JOB_TIMEOUT_MS` | 35 min | Max wall time per job |
| `CURSOR_AGENT.TRIAGE_TIMEOUT_MS` | 16 min | Max time for triage agent |
| `CURSOR_AGENT.INITIAL_TIMEOUT_MS` | 30 min | Max time for fix agent |
| `CURSOR_AGENT.FOLLOWUP_TIMEOUT_MS` | 15 min | Max time for build-fix followup agent |
| `POLLER.INTERVAL_MS` | 5 s | Jira poll interval |
| `POLLER.PR_COMMENT_INTERVAL_MS` | 5 s | PR comment poll interval |
| `RULES_LOADER.SIZE_LIMIT` | 32,000 chars | Total rules per repo per prompt |
| `RULES_LOADER.PER_FILE_LIMIT` | 8,000 chars | Per-file rules cap |
| `ATTACHMENTS.MAX_IMAGES` | 5 | Max Jira or PR comment images downloaded |
| `ATTACHMENTS.MAX_SIZE_BYTES` | 5 MB | Max size per downloaded attachment |
| `MEMORY.MAX_HISTORY_CHARS` | 8,000 chars | Max prior PR history injected into a follow-up prompt |
| `MEMORY.MAX_COMMENT_CHARS` | 500 chars | Max stored text per comment/reply in local PR memory |
| `SAFETY.MAX_DIFF_LINES` | 500 | Max lines changed in agent PR |
| `SAFETY.MAX_FILES_CHANGED` | 10 | Max files modified in agent PR |
| `SAFETY.MAX_FILES_DELETED` | 3 | Max files deleted in agent PR |

## Environments

Two deployed environments run from the same codebase; they differ by AWS account, GitHub App, and the `SANDBOX_MODE` flag.

**PRD (production)** — AWS `PRD` account. All pollers run on their normal schedules. Acts as `hu-agent[bot]`. `SANDBOX_MODE` is off: every Jira write (transitions, comments, subtasks, squad labels) and every PR is live. Deployed via the **`PRD Environment deployment`** GitHub Actions workflow.

**dev sandbox** — AWS `DEV` account. **Pollers are off — the service is endpoint-triggered only.** `SANDBOX_MODE=true`: all Jira writes are suppressed and PRs are opened as drafts. Runs a separate GitHub App (`hu-agent-dev[bot]`) so activity is clearly distinct from production. Slack output goes to the `hu-agent-test` channel (`C0B19DPH18R`). Deployed via the **`DEV Environment deployment`** workflow (`workflow_dispatch` with a `ref`, so any branch can be tested). Drive it by firing a ticket:

```bash
curl -sS -X POST "https://<dev-host>/hu-agent/api/pipeline/trigger" \
  -H "Content-Type: application/json" \
  -d '{"issueKey":"<KEY>"}'
```

The debug API only listens on the private ALB, so the curl above requires being on the corporate VPN. Alternatively, run the **`Trigger Fix Pipeline`** GitHub Actions workflow (`workflow_dispatch` with `environment` dev/prd + `issue_key`): the runner joins the VPN itself and POSTs to the same endpoint. Fire-and-forget — a green run means HTTP 202 (job enqueued); follow the outcome via Slack/Datadog/the resulting PR.

Any new pipeline write path (new Jira API call, new PR mutation) must be gated by `SANDBOX_MODE` the same way as existing writes.

## Operational Notes

- The service processes one job at a time through a single in-memory queue.
- Local repo clones live in `workdir/`, and local PR memory files live in `memory/`.
- Slack notifications are enabled only when both `SLACK_BOT_TOKEN` and `SLACK_CHANNEL_ID` are configured.
- PR comment attendance only applies to bot-authored PRs and skips comments from the bot itself.
- Jira polling and PR comment polling can be disabled independently through environment variables.

## Documentation

| Resource | Description |
|---|---|
| [Organizational Impact](https://www.notion.so/humand-co/Autonomous-Development-Agent-Organizational-Impact-3166757f3130806c8417cf839eb25aa6) | Internal Notion doc — business context, team impact, and strategic vision for the autonomous agent |
| [docs/github-pipeline.md](./docs/github-pipeline.md) | Detailed walkthrough of the GitHub PR Comment pipeline |
| [docs/jira-fix-pipeline.md](./docs/jira-fix-pipeline.md) | Detailed walkthrough of the Jira → Fix → PR pipeline |

## Docker

```bash
docker build -t hu-agent .
docker run -p 3000:3000 --env-file .env hu-agent
```
