This document provides a bird's-eye view of the Agent Please codebase. It is intended to help contributors (human and AI) orient themselves quickly and understand how the pieces fit together.
For the full specification, see SPEC.md. For the upstream reference implementation, see vendor/symphony/SPEC.md.
Agent Please is a long-running TypeScript daemon that turns issue tracker tasks into autonomous
Claude Code agent sessions. It continuously polls an issue tracker (GitHub Projects v2 or Asana),
creates an isolated workspace for each eligible issue, renders a Liquid prompt template, and
launches a Claude Code agent session inside that workspace via the
@anthropic-ai/claude-agent-sdk.
The service is primarily a scheduler/runner — it does not perform full ticket management. The orchestrator writes only status labels to GitHub issues. All state transitions, PR creation, and comments are performed by the Claude Code agent using tools available in the runtime environment.
| File | Purpose |
|---|---|
apps/agent-please/src/index.ts |
Binary entry point — calls runCli() |
apps/agent-please/src/cli.ts |
CLI argument parsing (run, init, --port) and Nuxt server startup |
packages/core/src/orchestrator.ts |
Core poll/dispatch/retry loop — start reading here for runtime behavior |
apps/agent-please/server/plugins/01.orchestrator.ts |
Nitro plugin: creates & starts the Orchestrator on server boot |
apps/agent-please/server/plugins/02.chat-bot.ts |
Nitro plugin: Chat SDK bot (GitHub + Slack adapters) |
apps/agent-please/server/plugins/03.auth.ts |
Nitro plugin: Better Auth initialization and admin bootstrap |
apps/agent-please/server/orpc/router.ts |
oRPC router — all typed API procedures (state, refresh, issues, sessions, projects) |
apps/agent-please/server/api/webhooks/github.post.ts |
POST /api/webhooks/github — GitHub webhook handler |
apps/agent-please/server/api/webhooks/slack.post.ts |
POST /api/webhooks/slack — Slack webhook handler |
apps/agent-please/server/api/webhooks/asana.post.ts |
POST /api/webhooks/asana — Asana webhook handler |
WORKFLOW.md |
User-authored config file in the target repository (not this repo) — defines tracker settings, hooks, agent limits, and the Liquid prompt template |
agent-please/ # Monorepo root (Bun + Turborepo)
├── apps/agent-please/ # Main Nuxt application (@pleaseai/agent)
│ ├── src/ # CLI entry point
│ │ ├── index.ts # Binary entry point — calls runCli()
│ │ ├── cli.ts # CLI parsing (Commander) → starts Nuxt server
│ │ └── init.ts # `agent-please init` — scaffolds GitHub Project + WORKFLOW.md
│ ├── app/ # Nuxt client-side application
│ │ ├── app.vue # Root component with <UApp>
│ │ ├── layouts/dashboard.vue # Nuxt UI Dashboard layout (sidebar + panels)
│ │ ├── pages/
│ │ │ ├── index.vue # Dashboard: metrics, running/retry tables
│ │ │ ├── issues/[identifier].vue # Issue detail: session, retry, events
│ │ │ ├── projects/ # Tracker project board view
│ │ │ ├── sessions/ # Session message viewer
│ │ │ └── login.vue # Auth login page
│ │ ├── components/ # StateBadge, RunningTable, RetryTable
│ │ ├── composables/ # useOrchestratorState, useIssueDetail, useProjectBoard, useProjects, useSessionMessages
│ │ └── utils/ # format.ts, types.ts
│ ├── server/ # Nitro server-side
│ │ ├── plugins/
│ │ │ ├── 01.orchestrator.ts # Nitro plugin: creates & starts Orchestrator
│ │ │ ├── 02.chat-bot.ts # Nitro plugin: Chat SDK (GitHub + Slack adapters)
│ │ │ └── 03.auth.ts # Nitro plugin: Better Auth initialization
│ │ ├── orpc/
│ │ │ ├── router.ts # oRPC typed procedures (state, refresh, issues, sessions, projects)
│ │ │ ├── middleware.ts # oRPC middleware (auth)
│ │ │ └── schemas.ts # Zod schemas for oRPC
│ │ ├── routes/rpc/ # Nitro route handler — mounts oRPC at /rpc/*
│ │ ├── api/
│ │ │ ├── auth/[...all].ts # Better Auth handler
│ │ │ ├── webhooks/github.post.ts # POST /api/webhooks/github
│ │ │ ├── webhooks/slack.post.ts # POST /api/webhooks/slack
│ │ │ └── webhooks/asana.post.ts # POST /api/webhooks/asana
│ │ └── utils/orchestrator.ts # useOrchestrator() helper
│ └── nuxt.config.ts # Nuxt config (Bun preset, Nuxt UI)
├── packages/core/ # @pleaseai/agent-core — orchestrator business logic
│ └── src/
│ ├── orchestrator.ts # Core loop: poll → reconcile → dispatch → retry
│ ├── config.ts # YAML front matter → typed ServiceConfig
│ ├── workflow.ts # WORKFLOW.md parser
│ ├── prompt-builder.ts # Liquid template rendering
│ ├── agent-runner.ts # Claude Code agent session via claude-agent-sdk
│ ├── workspace.ts # Per-issue directory management, git worktrees
│ ├── tools.ts # MCP tool server (asana_api, github_graphql)
│ ├── label.ts # GitHub label management
│ ├── filter.ts # Assignee and label filter matching
│ ├── webhook.ts # GitHub webhook verification + event filtering
│ ├── dispatch-lock.ts # Distributed dispatch lock (prevents duplicate agent dispatch)
│ ├── issue-comment-handler.ts # GitHub issue comment @mention dispatch handler
│ ├── relay-transport.ts # WebSocket relay transport (cloud relay mode)
│ ├── db.ts # Agent run history storage (Kysely — bun:sqlite or Turso)
│ ├── db-types.ts # Kysely database schema types
│ ├── server.ts # Standalone HTTP server (used without Nuxt)
│ ├── state.ts # Orchestrator state helpers
│ ├── logger.ts # Structured logging (consola withTag)
│ ├── agent-env.ts # Runtime env-var resolution for agent sessions
│ ├── session-renderer.ts # Session message extraction (claude-agent-sdk)
│ ├── types.ts # Shared type definitions
│ ├── tracker/ # Issue tracker adapters (GitHub, Asana)
│ └── index.ts # Barrel export
├── (external) @pleaseai/relay-client # WebSocket relay client (npm, from pleaseai/relay)
├── (external) @pleaseai/relay-server # PartyServer relay server (npm, from pleaseai/relay)
├── apps/relay-worker/ # @pleaseai/relay-worker — Cloudflare Worker deployment
├── apps/docs/ # @pleaseai/docs — Documentation site (Docus)
├── vendor/symphony/ # Upstream Symphony reference spec (read-only)
├── turbo.json # Turborepo task pipeline
├── eslint.config.ts # @antfu/eslint-config
└── tsconfig.json # TypeScript strict mode, ESNext target
WORKFLOW.md (target repo)
│
┌────▼────┐
│ Workflow │ Parse YAML front matter
│ Loader │ + Liquid prompt body
└────┬────┘
│
┌────▼────┐
│ Config │ Typed ServiceConfig with
│ Layer │ env-var ($VAR) resolution
└────┬────┘
│
┌──────────────▼──────────────┐
│ Orchestrator │
│ poll → reconcile → dispatch│
│ → retry (exponential) │
└──┬────────┬────────┬───────┘
│ │ │
┌───────▼──┐ ┌───▼───┐ ┌─▼──────────┐
│ Tracker │ │Workspace│ │ Agent │
│ Client │ │Manager │ │ Runner │───▶ DB (Kysely/Turso)
│(GitHub/ │ │(create,│ │(claude- │ run history
│ Asana) │ │ hooks, │ │ agent-sdk) │
└──────────┘ │worktree│ └────────────┘
└────────┘
┌──────────────────────────────┐
│ Chat SDK Bot │
│ GitHub adapter + Slack │
│ @mention → status response │
└──────────────────────────────┘
Before the first tick, start() calls startupTerminalWorkspaceCleanup(), which fetches all
issues in terminal states and removes their local workspaces. This keeps the workspace root clean
after a service restart.
Each poll tick executes in order:
- Reconcile — Refresh running issue states from the tracker; terminate workers for terminal/non-active issues; detect stalled agents.
- Validate — Re-check config validity (supports live reload via file watcher).
- Create tracker adapter — Instantiate a
TrackerAdapterfrom config. On adapter error, log and skip remaining steps. - Process watched states — Dispatch agents for watched-state issues (e.g.
Human Review) that have review activity (review decision or unresolved threads). - Fetch candidates — Poll active issues from the tracker with optional assignee/label filters.
- Sort and dispatch — Priority ascending, then oldest first. Check global and per-state concurrency limits. Create workspace, run hooks, start agent session.
- Schedule next tick — Wait
polling.interval_msbefore repeating.
createWorkspace()— Creates or reuses a per-issue directory (or git worktree if issue URL points to a GitHub repo). Runsafter_createhook on first creation.runBeforeRunHook()— Executes the optionalbefore_runshell hook.AppServerClient.startSession()— Validates workspace path againstworkspace.root(path traversal prevention) and assigns a local session UUID. No SDK communication occurs yet — the real session is established whenrunTurn()receives asystem/initevent.AppServerClient.runTurn()— Callsquery()from@anthropic-ai/claude-agent-sdkwith the rendered prompt. Translates SDK messages into orchestrator events (session_started,turn_completed,turn_failed,notification). Supports multi-turn: after each turn, refreshes issue state; continues if still active and undermax_turns.runAfterRunHook()— Executes the optionalafter_runshell hook.- On exit — normal exits schedule a 1s continuation retry; failures schedule exponential backoff
retries up to
max_retry_backoff_ms.
These constraints must hold across the codebase. Violating them is a bug.
-
Minimal tracker writes — The orchestrator does not write to the issue tracker except for the
LabelService(label.ts) which appliesdispatched/done/failedlabels to GitHub issues. All state transitions, PR operations, and comments are performed by the agent. -
Workspace path validation — Every workspace path must be validated against
config.workspace.rootbefore any agent launch. The path must be a strict child of the root (not equal to it), and symlink traversal is checked. SeevalidateWorkspacePath()andAppServerClient.validateWorkspaceCwd(). -
Config immutability during tick — The config object is replaced atomically on reload, never mutated in place. The orchestrator reads config at the start of each tick.
-
Concurrency limits are hard —
max_concurrent_agentsand per-state limits (max_concurrent_agents_by_state) are checked before every dispatch. A dispatch never exceeds these limits. -
Error types, not exceptions — Tracker operations, config validation, and workflow parsing return discriminated union error types (e.g.,
TrackerError,ValidationError,WorkflowError) instead of throwing. Check for'code' in resultbefore proceeding. -
Env-var indirection — Config values matching
$ENV_VARare resolved fromprocess.envat config build time. The resolved values (including secrets) are stored in theServiceConfigobject —$ENV_VARreferences do not persist past startup. -
No vendor modifications —
vendor/symphony/is a read-only submodule. It is excluded from linting, type-checking, and builds.
The codebase uses discriminated union types for expected errors rather than exceptions:
TrackerError— Network failures, API errors, unexpected payloads from GitHub/AsanaValidationError— Missing or invalid config fieldsWorkflowError— YAML parse errors, missing files, invalid front matterPromptBuildError— Liquid template parse/render failuresInitError— GitHub API failures duringagent-please init
Each error type has a code field for programmatic matching. The isTrackerError(),
isWorkflowError(), isPromptBuildError(), and isInitError() type guards are used
for narrowing.
- Runner: Bun test (Jest-compatible API)
- Pattern: Unit tests co-located with source files (
*.test.tsalongside*.ts) - Mocking:
AppServerClientaccepts an injectablequeryFnfor testing without the real Claude CLI. Tracker adapters are tested against mock GraphQL/REST responses. Workspace operations usespyOn(_git, 'spawnSync')to mock git commands. - Commands:
bun run test(all),bun run test:app(agent-please only)
Structured logging via consola with tag-based namespacing (createLogger('tag') in logger.ts).
Log lines are prefixed with module tags (e.g., [orchestrator], [db], [chat-bot]). Verbose
mode can be toggled with setVerbose() to increase log level.
Single-file configuration via WORKFLOW.md in the target repository:
- YAML front matter — Tracker connection, polling interval, workspace root, hooks, agent limits, Claude CLI settings
- Liquid template body — Prompt template rendered with issue context variables
- Live reload — File watcher triggers re-parse; invalid configs are rejected with the last known-good config retained
| Tracker | Method | Config |
|---|---|---|
| GitHub Projects v2 | PAT | tracker.api_key or $GITHUB_TOKEN |
| GitHub Projects v2 | GitHub App | tracker.app_id + tracker.private_key + tracker.installation_id |
| Asana | PAT | tracker.api_key or $ASANA_ACCESS_TOKEN |
GitHub App auth uses @octokit/auth-app to generate installation tokens. When both PAT and App
credentials are present, PAT takes precedence.
The agent runner injects tracker-specific MCP tools into each Claude Code session:
github_graphql— Raw GraphQL queries/mutations using the service's authenticated Octokitasana_api— Raw REST API calls using the service's Bearer token
These allow the agent to perform tracker writes (state transitions, comments) without needing separate credentials.
The Chat SDK plugin (02.chat-bot.ts) initializes platform adapters based on available
configuration:
- GitHub adapter — Requires
github_projectstracker config + webhook secret. Handles@mentionin issue comments via/api/webhooks/github. - Slack adapter — Requires
SLACK_BOT_TOKEN+SLACK_SIGNING_SECRETenv vars. Handles events via/api/webhooks/slack.
On @mention, the bot responds with current orchestrator status (running/retrying issues, token
usage). The bot lifecycle is managed by the Nitro plugin (startup/shutdown).
db.ts provides persistent storage for agent run records using Kysely:
- Embedded mode — Local SQLite file via
bun:sqlite+kysely-bun-sqlite(default:.agent-please/agent_runs.db) - Cloud mode — Turso remote database via
@libsql/kysely-libsqlusingdb.turso_url+db.turso_auth_tokenconfig
Records include issue identifier, session ID, duration, token usage, turn count, and status.
The session messages API (/api/v1/sessions/:sessionId/messages) uses session-renderer.ts to
extract and format messages from the Claude Agent SDK.