# DevIntern: Full Documentation > DevIntern is a self-hosted worker that turns ready tracker tickets into pull requests with the coding agents you already use. `@devintern/pm` is optional intake for drafting better tickets. # Code # Implement Jira Issues with @devintern/code @devintern/code implements work from Jira Cloud issues: fetch issue details, comments, and attachments, run a feasibility check, transition statuses, execute your AI agent, commit changes, open pull requests, and post results back to the issue. This integration targets **Jira Cloud** (`*.atlassian.net`). Jira Data Center / Server is not supported. ## How It Works @devintern/code uses the [Jira Cloud REST API](https://developer.atlassian.com/cloud/jira/platform/rest/v3/) to read and update issues you pass on the command line or select with JQL. - Issue descriptions are converted from **Atlassian Document Format (ADF)** to markdown for your AI agent - Comments from humans are included; prior @devintern/code comments are filtered out to reduce noise - Attachments are downloaded into the task output directory when present - Status transitions use workflow status **names** from your `settings.json` (for example, "In Progress", "In Review") - After a run, feasibility, implementation, or incomplete summaries are posted as Jira comments ## Setup ### 1. Initialize project config From your repository root: ```bash devintern init ``` This creates `.devintern-code/.env` and `.devintern-code/settings.json`. ### 2. Set Jira credentials In `.devintern-code/.env`: ```bash TASK_TRACKER=jira JIRA_BASE_URL=https://your-org.atlassian.net JIRA_EMAIL=your-email@example.com JIRA_API_TOKEN=your-api-token ``` `TASK_TRACKER` defaults to `jira` if omitted. All three Jira variables are required. ### 3. Create an API token 1. Go to [Atlassian API tokens](https://id.atlassian.com/manage-profile/security/api-tokens) 2. Click **Create API token**, add a label (e.g. `DevIntern`), and copy the token 3. Paste the token into `JIRA_API_TOKEN` Use the **email address** of the Atlassian account that owns the token for `JIRA_EMAIL`. Authentication is Basic auth (`email:token`). Store credentials in `.devintern-code/.env`. That file should stay out of version control (`devintern init` adds it to `.gitignore`). ### 4. Find your Jira site URL Your site URL is the hostname you use to open Jira in the browser: ``` https://your-org.atlassian.net/jira/... └─ JIRA_BASE_URL ─┘ ``` Set `JIRA_BASE_URL` to the full URL **without** a trailing slash. ### 5. Configure status transitions (optional) Edit `.devintern-code/settings.json` using the **project key** as the lookup key (the prefix in issue keys, e.g. `PROJ` in `PROJ-123`): ```json { "jira": { "projects": { "PROJ": { "inProgressStatus": "In Progress", "todoStatus": "To Do", "prStatus": "In Review", "storyPointsField": "customfield_10016" } } } } ``` @devintern/code transitions issues to: - **inProgressStatus** when implementation starts (after the clarity check) - **prStatus** after a pull request is created - **todoStatus** when implementation is incomplete or max turns are reached Status names must match your Jira workflow exactly. Omit a field to skip that transition. See [Configuration](./configuration.md) for legacy `projects` format and other settings. ## Running tasks ### Single issue ```bash devintern PROJ-123 devintern PROJ-123 --create-pr devintern PROJ-123 --skip-clarity-check --create-pr ``` ### Multiple issues ```bash devintern PROJ-123 PROJ-124 PROJ-125 --create-pr ``` ### Batch with JQL ```bash devintern --jql "project = PROJ AND status = 'To Do'" --create-pr devintern --query "assignee = currentUser() AND status = 'To Do'" --create-pr ``` `--query` is the preferred flag; `--jql` remains available with a deprecation warning. ### Story points estimation Batch mode for AI-assisted estimation (also available for Linear): ```bash devintern PROJ-123 --estimate devintern --estimate --query "project = PROJ AND status = 'To Do'" ``` See [Story Points Estimation](./story-points-estimation.md) for field discovery, re-estimation, and automation examples. ## What Gets Posted | Stage | Jira comment | | ------------------------- | -------------------------------------------------------- | | Feasibility check | Automated Task Feasibility Assessment (markdown summary) | | Successful implementation | Implementation Completed by @devintern/code | | Incomplete / max turns | Implementation Incomplete | | Estimation (`--estimate`) | Automated Story Points Estimation | Use `--skip-comments` to skip all Jira comments and status transitions for a run. ## Permissions Your Jira user needs, at minimum: - **Browse projects** and **View issues** on target projects - **Transition issues** if you configure status changes - **Add comments** unless you use `--skip-comments` - **Edit issues** for story points when using `--estimate` ## Troubleshooting **"Missing required JIRA environment variables"** Set `JIRA_BASE_URL`, `JIRA_EMAIL`, and `JIRA_API_TOKEN` in `.devintern-code/.env`. **"Issue not found"** - Verify the issue key exists and you have access - Confirm `JIRA_BASE_URL` matches your Cloud site - Check that `JIRA_EMAIL` matches the account that created the API token **Jira API error (401)** - API token is invalid or revoked. Create a new token at [Atlassian API tokens](https://id.atlassian.com/manage-profile/security/api-tokens) - `JIRA_EMAIL` does not match the token owner **Jira API error (403)** - Your account lacks permission on the issue or project - Ask a Jira admin to grant browse, comment, or transition permissions **Status transition failed** - The status name in `settings.json` does not match your workflow (check spelling and capitalization) - The transition may not be valid from the issue's current status - @devintern/code logs a warning and continues the run when a transition fails **JQL returns no issues** - Test the query in Jira's issue search first - Quote project names and statuses that contain spaces: `project = "My Project"` **Descriptions look wrong in agent context** @devintern/code converts ADF to markdown. Unusual ADF structures may lose formatting; simplify the issue description in Jira if needed. --- # Implement Asana Tasks with @devintern/code @devintern/code can implement work directly from Asana tasks: fetch task details and comments, run a feasibility check, move the task between project sections, execute your AI agent, commit changes, open a pull request, and post results back as task comments. ## Prerequisites - [Bun](https://bun.sh) and `@getdevintern/code` installed globally - Asana personal access token - Git repository for your project ## Setup ### 1. Set the task tracker In `.devintern-code/.env`: ```bash TASK_TRACKER=asana ``` ### 2. Add Asana credentials ```bash ASANA_API_TOKEN=your-asana-pat ASANA_DEFAULT_PROJECT_GID=1200000000000000 ``` Create the personal access token in [Asana developer settings](https://app.asana.com/0/my-apps). `ASANA_DEFAULT_PROJECT_GID` is optional but recommended: it anchors section transitions and batch queries to a specific project. Find the GID in the project URL. ### 3. Configure section transitions Asana has no global workflow states, so @devintern/code maps statuses to project sections. Configure them in `.devintern-code/settings.json` using the project GID as the project key: ```json { "asana": { "projects": { "1200000000000000": { "inProgressStatus": "In Progress", "todoStatus": "To Do", "prStatus": "In Review" } } } } ``` Section names must match your project's sections (case-insensitive). Transitioning to `done`, `completed`, or `closed` marks the task complete instead of moving sections. ## Running a task Pass a task GID or a full task URL: ```bash # Task GID devintern 1200000000000001 --create-pr # Full task URL devintern https://app.asana.com/0/1200000000000000/1200000000000001 --create-pr ``` This workflow: 1. Fetches the task notes, tags, attachments, and comments 2. Runs a feasibility assessment (skippable with `--skip-clarity-check`) 3. Moves the task to the `inProgressStatus` section (unless `--skip-comments` is set) 4. Creates a feature branch, runs your agent, commits, and optionally opens a PR 5. Moves the task to the `prStatus` section after PR creation 6. Posts implementation or assessment comments on the task ## Batch processing with --query Asana has no query language, so @devintern/code provides a small filter syntax: ```bash devintern --query 'section:"To Do" completed:false' --create-pr devintern --query 'project:1200000000000000 assignee:Ada login bug' --create-pr ``` Supported filters: `project:` (defaults to `ASANA_DEFAULT_PROJECT_GID`), `section:""`, `assignee:`, `completed:true|false`. Any remaining text matches task names (case-insensitive). The integration lists the project's tasks (first 100) and filters them locally, which keeps it working on free Asana plans (the workspace search API requires Premium). ## Story points estimation Asana has no built-in estimation field. If your project has a numeric custom field for points, name it in `.devintern-code/.env`: ```bash ASANA_STORY_POINTS_FIELD=Story Points ``` With the field configured, `devintern --estimate` sets the custom field value and posts an estimation comment. Without it, estimates are posted as comments only. ## Limitations - **Batch size:** queries read the first 100 tasks of a project. Narrow with sections for larger projects. - **Rich text:** comments are posted as Asana rich text with a plain-text fallback when formatting is rejected. - **Comments:** use `--skip-comments` to skip task comments and section moves for a run. ## Troubleshooting **"Missing required Asana environment variables"** Ensure `ASANA_API_TOKEN` is set in `.devintern-code/.env`. **"Cannot resolve a project for task"** The task is not in any project and no `ASANA_DEFAULT_PROJECT_GID` is set. Add the task to a project or set the default project GID. **"Section \"In Progress\" not found in the project"** Check the section names in your project against `settings.json`. Available sections are listed in the error message. **"Asana task search requires a project"** Pass `project:` in the query or set `ASANA_DEFAULT_PROJECT_GID`. --- # Implement Azure DevOps Work Items with @devintern/code @devintern/code can implement work directly from Azure DevOps work items: fetch work item details and comments, run a feasibility check, move the item through workflow states, execute your AI agent, commit changes, open a pull request, and post results back to the work item. ## Prerequisites - [Bun](https://bun.sh) and `@getdevintern/code` installed globally - Azure DevOps Personal Access Token (PAT) with Work Items read/write - Git repository for your project ## Setup ### 1. Set the task tracker In `.devintern-code/.env`: ```bash TASK_TRACKER=azure-devops ``` ### 2. Add Azure DevOps credentials ```bash AZURE_DEVOPS_ORG=your-organization AZURE_DEVOPS_PAT=your-pat-token AZURE_DEVOPS_PROJECT=YourProject ``` The organization is the segment after `dev.azure.com/` in your URLs. Create the PAT under User settings, Personal access tokens, with the **Work Items (Read and write)** scope. The project is the project name that contains your work items. ### 3. Configure state transitions Edit `.devintern-code/settings.json` using the project name as the project key: ```json { "azure-devops": { "projects": { "YourProject": { "inProgressStatus": "Active", "todoStatus": "New", "prStatus": "Resolved" } } } } ``` State names must match the workflow states of your work item types (for example `New`, `Active`, `Resolved`, `Closed` in the Agile template). @devintern/code moves work items to: - **inProgressStatus** when implementation starts (after the clarity check) - **prStatus** after a pull request is created - **todoStatus** when implementation is incomplete or max turns are reached See [Configuration](./configuration.md) for all settings fields. ## Running a work item Pass a numeric work item ID or a full work item URL: ```bash # Work item ID devintern 4211 --create-pr # Full work item URL devintern https://dev.azure.com/your-org/YourProject/_workitems/edit/4211 --create-pr ``` This workflow: 1. Fetches the work item description (HTML converted to markdown), tags, attachments, and comments 2. Runs a feasibility assessment (skippable with `--skip-clarity-check`) 3. Moves the item to `inProgressStatus` (unless `--skip-comments` is set) 4. Creates a feature branch, runs your agent, commits, and optionally opens a PR 5. Moves the item to `prStatus` after PR creation 6. Posts implementation or assessment comments on the work item Parent, child, and related work item links are fetched and included in the agent context. ## Batch processing with --query Select multiple work items with [WIQL](https://learn.microsoft.com/en-us/azure/devops/boards/queries/wiql-syntax), scoped to your configured project: ```bash devintern --query "SELECT [System.Id] FROM WorkItems WHERE [System.State] = 'New'" --create-pr devintern --query "SELECT [System.Id] FROM WorkItems WHERE [System.Tags] CONTAINS 'bug'" --create-pr ``` The first 100 matching work items are processed in sequence. ## Story points estimation Estimation mode is fully supported via the `Microsoft.VSTS.Scheduling.StoryPoints` field (the Agile template default): ```bash devintern 4211 --estimate ``` If your process template uses a different field (for example `Effort` in Scrum), set `storyPointsField` in `settings.json` for the project. ## Limitations - **Description format:** work item descriptions are HTML. They are converted to markdown for the agent, and automation comments are converted from markdown to HTML before posting. - **Comments API:** Azure DevOps work item comments use a preview API version (7.1-preview.3), pinned by the integration. - **Comments:** use `--skip-comments` to skip work item comments and state transitions for a run. ## Troubleshooting **"Missing required Azure DevOps environment variables"** Ensure `AZURE_DEVOPS_ORG`, `AZURE_DEVOPS_PAT`, and `AZURE_DEVOPS_PROJECT` are set in `.devintern-code/.env`. **"Failed to move work item ... to state"** State names differ per process template and work item type. Check the states available for the work item type in your project settings and match them exactly in `settings.json`. **401 or 403 errors** Verify the PAT has not expired and includes the Work Items (Read and write) scope for the target organization. --- # Implement GitHub Issues with @devintern/code @devintern/code can implement work directly from GitHub Issues: fetch issue details and comments, run a feasibility check, move status labels, execute your AI agent, commit changes, open a pull request in the same repository, and post results back on the issue. Looking for unattended PR review automation? Use the [worker](./worker.md) (polling plus the [relay](./relay.md)). Direct GitHub webhooks are an advanced path: [GitHub Integration](./github-integration.md). ## Prerequisites - [Bun](https://bun.sh) and `@getdevintern/code` installed globally - GitHub personal access token - Git repository for your project ## Setup ### 1. Set the task tracker In `.devintern-code/.env`: ```bash TASK_TRACKER=github ``` ### 2. Add GitHub credentials ```bash GITHUB_TOKEN=ghp_xxxxxxxxxxxx GITHUB_REPO=owner/repo ``` The same `GITHUB_TOKEN` used for pull request creation works here. It needs the `repo` scope (classic token) or `Issues: Read and write` plus `Pull requests: Read and write` (fine-grained token). `GITHUB_REPO` is the repository whose issues you want to implement, in `owner/repo` form. GitHub App credentials (`GITHUB_APP_ID` + private key) cannot substitute for this tracker. The Issues client only accepts `GITHUB_TOKEN`. Standard relay-backed workspaces use that token locally and the central DevIntern AI App for events. Customer-owned App credentials are only for advanced no-relay installations. See [Configuration](./configuration.md#github-authentication). ### 3. Configure status labels GitHub has no built-in workflow states, so @devintern/code maps statuses to labels. Create the labels in your repository, then configure them in `.devintern-code/settings.json` using `owner/repo` as the project key: ```json { "github": { "projects": { "acme/webapp": { "inProgressStatus": "In Progress", "todoStatus": "To Do", "prStatus": "In Review" } } } } ``` To keep statuses mutually exclusive, also list them in `.devintern-code/.env`: ```bash GITHUB_STATUS_LABELS=To Do,In Progress,In Review ``` When a status changes, @devintern/code adds the target label and removes the other labels in this list. Transitioning to `closed` or `done` closes the issue instead of applying a label. ## Running an issue Pass an issue number, `#number`, or a full issue URL: ```bash # Issue number devintern 123 --create-pr # Full issue URL devintern https://github.com/acme/webapp/issues/123 --create-pr ``` This workflow: 1. Fetches the issue body, labels, and comments 2. Runs a feasibility assessment (skippable with `--skip-clarity-check`) 3. Applies the `inProgressStatus` label (unless `--skip-comments` is set) 4. Creates a feature branch, runs your agent, commits, and optionally opens a PR 5. Applies the `prStatus` label after PR creation 6. Posts implementation or assessment comments on the issue ## Batch processing with --query Select multiple issues with [GitHub search qualifiers](https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests). The query is automatically scoped to your repository with `repo:owner/repo is:issue`: ```bash devintern --query "is:open label:bug" --create-pr devintern --query 'is:open "login flow"' --create-pr ``` The first 100 matching issues are processed in sequence. Note that GitHub's search API is rate-limited to 30 requests per minute. ## Story points estimation GitHub Issues has no estimation field, so `--estimate` runs in comment-only mode: the analysis is posted (or updated) as an issue comment with the suggested points, reasoning, risks, and unclear areas. ## Limitations - **Attachments:** GitHub has no attachment API. Images and files embedded in the issue body (`user-attachments` links) are downloaded for the agent. - **Status labels:** labels named in `settings.json` must already exist in the repository. The error message lists available labels when one is missing. - **Comments:** use `--skip-comments` to skip issue comments and label transitions for a run. ## Troubleshooting **"Missing required GitHub environment variables"** Ensure `GITHUB_TOKEN` and `GITHUB_REPO` are set in `.devintern-code/.env`. A GitHub App ID and private key are not enough for this tracker. **"Label \"In Progress\" not found in the repository"** Create the label in your repository (Issues → Labels) or change the status names in `settings.json` to match existing labels. **Old status labels pile up on issues** Set `GITHUB_STATUS_LABELS` to the full list of status label names so transitions remove the previous status. **Search returns pull requests** Queries are scoped with `is:issue` automatically. If you pass your own `repo:` qualifier, include `is:issue` yourself. --- # Implement Linear Issues with @devintern/code @devintern/code can implement work directly from Linear issues: fetch issue details and comments, run a feasibility check, move the issue through your workflow states, execute your AI agent, commit changes, open a pull request, and post results back to the issue. ## Prerequisites - [Bun](https://bun.sh) and `@getdevintern/code` installed globally - Linear Personal API key - Git repository for your project ## Setup ### 1. Set the task tracker In `.devintern-code/.env`: ```bash TASK_TRACKER=linear ``` ### 2. Add your Linear API key ```bash LINEAR_API_KEY=lin_api_xxxxxxxxxxxx ``` Create the key at [Linear API settings](https://linear.app/settings/api): under **Personal API keys**, click **Create key**, add a label (e.g. `DevIntern`), and copy the key. It starts with `lin_api_` and cannot be viewed again after you leave the page. The key inherits your Linear account permissions. ### 3. Configure workflow state transitions Edit `.devintern-code/settings.json` using the team key (the prefix in identifiers like `ENG-42`) as the project key: ```json { "linear": { "projects": { "ENG": { "inProgressStatus": "In Progress", "todoStatus": "Backlog", "prStatus": "In Review" } } } } ``` State names must match your team's workflow states (case-insensitive). @devintern/code moves issues to: - **inProgressStatus** when implementation starts (after the clarity check) - **prStatus** after a pull request is created - **todoStatus** when implementation is incomplete or max turns are reached See [Configuration](./configuration.md) for all settings fields. ## Running an issue Pass one or more issue identifiers or full issue URLs. Identifiers are case-insensitive (`dan-6` is the same as `DAN-6`). Multiple keys are processed in order: ```bash # Identifier devintern ENG-42 --create-pr # Several issues in one run devintern dan-6 dan-7 dan-8 --create-pr # Full issue URL devintern https://linear.app/acme/issue/ENG-42/fix-login-bug --create-pr ``` This workflow: 1. Fetches the issue description, labels, attachments, and comments 2. Runs a feasibility assessment (skippable with `--skip-clarity-check`) 3. Moves the issue to `inProgressStatus` (unless `--skip-comments` is set) 4. Creates a feature branch, runs your agent, commits, and optionally opens a PR 5. Moves the issue to `prStatus` after PR creation 6. Posts implementation or assessment comments on the issue ## Batch processing with --query Select multiple issues with a [Linear IssueFilter](https://studio.linear.app/graphql) expressed as JSON: ```bash devintern --query '{"state":{"name":{"eq":"Todo"}},"team":{"key":{"eq":"ENG"}}}' --create-pr ``` Plain text works too and matches against issue titles (case-insensitive contains): ```bash devintern --query "login bug" --create-pr ``` The first 50 matching issues are processed in sequence. ## Story points estimation Linear has a native estimate field, so estimation mode is fully supported: ```bash devintern ENG-42 --estimate ``` This analyzes the issue, sets the native estimate value, and posts (or updates) an estimation comment with reasoning, risks, and unclear areas. ## Limitations - **Attachments:** files hosted on `uploads.linear.app` are downloaded with your API key. External attachment links (Figma, Google Docs) are passed to the agent as links. - **Comments:** use `--skip-comments` to skip Linear comments and state transitions for a run. ## Troubleshooting **"Missing required Linear environment variables"** Ensure `LINEAR_API_KEY` is set in `.devintern-code/.env`. **Issue does not move between states** Confirm the state names in `settings.json` match your team's workflow states, and that the project key is the team key from the issue identifier (`ENG` for `ENG-42`). **"Workflow state \"In Progress\" not found for team"** Check spelling against your team's workflow states in Linear settings. Available states are shown in the error message. **"Invalid Linear IssueFilter JSON"** Quote the JSON filter in single quotes in your shell and validate it against the IssueFilter schema in the [Linear GraphQL explorer](https://studio.linear.app/graphql). --- # Implement Trello Cards with @devintern/code @devintern/code can implement work directly from Trello cards: fetch card details and comments, run a feasibility check, move the card between lists, execute your AI agent, commit changes, open a pull request, and post results back to the card. ## Prerequisites - [Bun](https://bun.sh) and `@getdevintern/code` installed globally - Trello Power-Up API key and user token with read/write access - Git repository for your project ## Setup ### 1. Set the task tracker In `.devintern-code/.env`: ```bash TASK_TRACKER=trello ``` ### 2. Add Trello credentials Both variables are required for @devintern/code: ```bash TRELLO_API_KEY=your-power-up-api-key TRELLO_API_TOKEN=your-user-token ``` **API key:** Create or open a Power-Up at [trello.com/power-ups/admin](https://trello.com/power-ups/admin), then copy the API key from the **API Key** tab. **API token:** Generate a token by visiting (replace `YOUR_API_KEY` with your key): ``` https://trello.com/1/authorize?expiration=never&scope=read,write&response_type=token&name=DevIntern&key=YOUR_API_KEY ``` Click **Allow**, then copy the token from the page. Optionally pin a default board: ```bash TRELLO_DEFAULT_BOARD_ID=abc123 # board short ID from trello.com/b/{boardId}/board-name ``` `TRELLO_DEFAULT_BOARD_ID` is used for settings lookup when the board cannot be inferred from the card. Status transitions are driven entirely by the list names you configure in `settings.json` (see below), not by an environment variable. ### 3. Configure list transitions Edit `.devintern-code/settings.json` using the board short ID as the project key: ```json { "trello": { "projects": { "abc123": { "inProgressStatus": "Doing", "todoStatus": "To Do", "prStatus": "Review" } } } } ``` List names must match your board exactly (case-insensitive). @devintern/code moves cards to: - **inProgressStatus** when implementation starts (after the clarity check) - **prStatus** after a pull request is created - **todoStatus** when implementation is incomplete or max turns are reached See [Configuration](./configuration.md) for all settings fields. ## Running a card Pass a card short link, full URL, or 24-character card ID: ```bash # Short link devintern 4uWKPOTv --create-pr # Full card URL devintern https://trello.com/c/4uWKPOTv/card-slug --create-pr ``` This workflow: 1. Fetches card description, labels, and comments 2. Runs a feasibility assessment (skippable with `--skip-clarity-check`) 3. Moves the card to `inProgressStatus` (unless `--skip-comments` is set) 4. Creates a feature branch, runs your agent, commits, and optionally opens a PR 5. Moves the card to `prStatus` after PR creation 6. Posts implementation or assessment comments on the card ## Batch processing with --query Select multiple cards with [Trello search operators](https://support.atlassian.com/trello/docs/searching-for-cards-all-boards/), scoped to `TRELLO_DEFAULT_BOARD_ID` when set: ```bash devintern --query 'list:"To Do" is:open' --create-pr devintern --query 'label:bug login' --create-pr ``` The first 100 matching cards are processed in sequence. ## Limitations - **Story points:** Estimation mode (`--estimate`) is not supported for Trello (no estimation concept). Estimation works with Jira, Linear, Azure DevOps, and Asana. - **Comments:** Use `--skip-comments` to skip Trello comments and list transitions for a run. ## Troubleshooting **"Missing required Trello environment variables"** Ensure `TRELLO_API_KEY` and `TRELLO_API_TOKEN` are set in `.devintern-code/.env`. **Card does not move between lists** Confirm list names in `settings.json` match your board, and that the project key is the board short ID (from the board URL, not the 24-character internal ID). Set `TRELLO_DEFAULT_BOARD_ID` to the same short ID if needed. **"List \"Doing\" not found on board"** Check spelling and capitalization against the lists on your Trello board. Available lists are shown in the error message. **Transitions skipped entirely** List moves are tied to comment posting. Do not pass `--skip-comments` if you expect cards to move. --- # Implement GitLab Issues with @devintern/code @devintern/code can implement work directly from GitLab issues: fetch issue details and comments, run a feasibility check, move status labels, execute your AI agent, commit changes, and post results back on the issue. Both **GitLab Cloud** and **self-hosted instances** are supported. ## Prerequisites - [Bun](https://bun.sh) and `@getdevintern/code` installed globally - GitLab personal access token with the `api` scope - Git repository for your project ## Setup ### 1. Set the task tracker In `.devintern-code/.env`: ```bash TASK_TRACKER=gitlab ``` ### 2. Add GitLab credentials ```bash # Cloud default — omit for gitlab.com; set for self-hosted: GITLAB_BASE_URL=https://gitlab.example.com GITLAB_TOKEN=glpat_xxxxxxxxxxxx GITLAB_PROJECT=group/sub/repo ``` - `GITLAB_BASE_URL` — instance root URL. Omit for GitLab Cloud (`https://gitlab.com` is the default). Self-hosted instances keep their protocol, so internal `http://` hosts work. - `GITLAB_TOKEN` — personal access token from `/-/user_settings/personal_access_tokens` on the same instance, with the **`api`** scope. - `GITLAB_PROJECT` — project path (`group/repo`, subgroups allowed: `group/sub/repo`) or a numeric project ID. ### 3. Configure status labels Like GitHub Issues, GitLab has no built-in workflow states that map cleanly across teams, so @devintern/code maps statuses to labels. Create the labels in your project, then configure them in `.devintern-code/settings.json` using the project path as the key: ```json { "gitlab": { "projects": { "acme/team/webapp": { "inProgressStatus": "In Progress", "todoStatus": "To Do", "prStatus": "In Review" } } } } ``` To keep statuses mutually exclusive, also list them in `.devintern-code/.env`: ```bash GITLAB_STATUS_LABELS=To Do,In Progress,In Review ``` When a status changes, @devintern/code adds the target label and removes the other labels in this list. Transitioning to `closed` or `done` closes the issue instead of applying a label; moving back to an open status reopens it. ## Running an issue Pass an issue number, `#number`, a `group/sub/repo#123` reference, or a full issue URL: ```bash # Issue number devintern 123 --create-pr # Full issue URL (self-hosted URLs work too) devintern https://gitlab.com/acme/team/webapp/-/issues/123 --create-pr ``` This workflow: 1. Fetches the issue body, labels, and comments 2. Runs a feasibility assessment (skippable with `--skip-clarity-check`) 3. Applies the `inProgressStatus` label (unless `--skip-comments` is set) 4. Creates a feature branch, runs your agent, commits, and optionally opens a PR 5. Applies the `prStatus` label after PR creation 6. Posts implementation or assessment comments on the issue ## Batch processing with --query Select multiple issues with familiar qualifiers — @devintern/code translates them to GitLab's [list issues](https://docs.gitlab.com/ee/api/issues.html#list-project-issues) filters. Queries are always scoped to `GITLAB_PROJECT`: ```bash devintern --query "is:open label:bug" --create-pr devintern --query 'is:open "login flow"' --create-pr devintern --query "assignee:@me" --create-pr ``` Supported qualifiers: `is:open` / `is:closed`, `label:name` (repeatable), `assignee:@me` / `assignee:username`, `updated:>=`. Anything else is free-text search. The first 100 matching issues are processed in sequence. ## Story points estimation GitLab issues have no estimation field, so `--estimate` runs in comment-only mode: the analysis is posted (or updated) as an issue comment with the suggested points, reasoning, risks, and unclear areas. ## Token scopes for Cloud vs. self-hosted | Scope | Needed for | | ---------- | ------------------------------------------------------------------------------------------ | | `api` | Full read/write access (recommended) | | `read_api` | Read-only setups (fetching issues works; posting comments and label transitions will fail) | Self-hosted tokens only exist on their own instance — a gitlab.com token cannot authenticate against your on-premises GitLab and vice versa. ## Limitations - **Attachments:** files embedded in issue bodies (`/uploads/...` links) are downloaded for the agent using your token; other external links stay as references. - **Status labels:** labels named in `settings.json` must already exist in the project. The error message lists available labels when one is missing. - **Comments:** use `--skip-comments` to skip issue comments and label transitions for a run. - **Pull requests:** PR creation targets GitHub/Bitbucket remotes today; GitLab merge-request automation is not part of this integration yet. ## Troubleshooting **"Missing required GitLab credentials"** Ensure `GITLAB_TOKEN` and `GITLAB_PROJECT` are set in `.devintern-code/.env`. **"GitLab API error (401)"** Token rejected: check that it was created on the same instance as `GITLAB_BASE_URL`, has not expired, and carries the `api` scope. **"Label \"In Progress\" not found in the project"** Create the label in your project (Issues → Labels) or change the status names in `settings.json` to match existing labels. **Old status labels pile up on issues** Set `GITLAB_STATUS_LABELS` to the full list of status label names so transitions remove the previous status. --- # Markdown File Tasks You can run `devintern` against a local markdown file instead of a Jira issue or Trello card. No PM credentials are required when every argument is a file path. This is useful for one-off tasks, local specs, or projects that do not use an external task tracker. ## Direct file path mode Pass one or more `.md` file paths (relative or absolute) directly as arguments: ```bash # Single file devintern ./tasks/feature-spec.md # With PR creation devintern ./tasks/feature-spec.md --create-pr # Multiple files (processed in sequence) devintern ./epic.md ./subtask-a.md ./subtask-b.md # Skip branch creation devintern ./tasks/feature-spec.md --no-git ``` Any argument ending in `.md` or containing a path separator (`/` or `./`) is treated as a file path. Jira and Trello keys that do not match are routed to the configured task tracker as normal, so you can mix both in one command: ```bash # PM task + local file in one run devintern PROJ-123 ./extra-context.md ``` When all arguments are file paths, `devintern` skips PM credential validation entirely so no `.devintern-code/.env` is needed for the task tracker section. ## Markdown file format The file is passed directly to the agent. Optionally add YAML frontmatter to control the task key and status tracking: ```markdown --- key: my-feature status: To Do type: Feature --- # Add user profile page Add a profile page at `/profile` that shows the signed-in user's name and avatar. ## Acceptance criteria - Route `/profile` renders user name and avatar - Redirects to `/login` when not authenticated ``` ### Frontmatter fields | Field | Required | Description | | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `key` | No | Task key used for the git branch name and output directory. Defaults to the filename stem (e.g. `feature-spec` for `feature-spec.md`). | | `status` | No | Current status. When present, `devintern` updates it to `In Progress` before running the agent and to `Done` on success. | | `type` | No | Issue type label (informational only; defaults to `Task`). | | `created_at` | No | Creation timestamp (informational only). | Frontmatter is optional. A file with no frontmatter works fine: the filename stem is used as the task key and no status tracking is applied. ### Title extraction The task title is taken from the first `# H1` heading in the file. If no H1 is present, the filename stem is used as the title. ### Implementation instructions If the file does not already contain an `## Implementation Instructions` section, `devintern` appends a short prompt at the end of the content passed to the agent. You can include your own instructions in the file to override this. ## Git branch name The branch is created as `feature/{key}` where `{key}` is: 1. The `key:` frontmatter value (if set), or 2. The filename stem (e.g. `my-task.md` produces `feature/my-task`) Special characters are replaced with hyphens for git compatibility. ## Status tracking When the frontmatter includes a `status:` field, `devintern` updates it in place: - Set to `In Progress` just before the agent runs - Set to `Done` after a successful run (detected by the presence of `implementation-summary.md`) - Left as `In Progress` if the run fails or is incomplete The file is edited in place, so commit it to version control if you want to track these transitions. ## TASK_TRACKER=markdown mode For projects that store all task files in a single directory, you can set the tracker to `markdown` and pass short task keys instead of file paths: ```bash # .devintern-code/.env TASK_TRACKER=markdown MARKDOWN_TASKS_DIR=/path/to/tasks ``` Then run: ```bash devintern my-feature --create-pr ``` `devintern` resolves `my-feature` to `{MARKDOWN_TASKS_DIR}/my-feature.md` (or uses the `key:` frontmatter field to find the file). All standard flags work the same way. `MARKDOWN_TASKS_DIR` is required when `TASK_TRACKER=markdown`. ### Batch processing with --query Select multiple task files by frontmatter fields, with optional free text matched against titles: ```bash devintern --query "status=todo" --create-pr devintern --query 'status="In Progress" type=bug login' --create-pr ``` Each space-separated `key=value` pair must match the file's frontmatter (values compared case-insensitively; quote values containing spaces). Files without frontmatter only match queries with no filters. ## What is and is not supported | Feature | File path mode | TASK_TRACKER=markdown | | ------------------------ | ---------------------------------- | ------------------------- | | No PM credentials needed | Yes | Yes | | Status field auto-update | Yes (if frontmatter has `status:`) | Yes | | Git branch creation | Yes | Yes | | `--create-pr` | Yes | Yes | | `--auto-review` | Yes | Yes | | `--skip-clarity-check` | Yes | Yes | | `--no-git` | Yes | Yes | | Batch `--query` | No | Yes (frontmatter filters) | | Post comments to tracker | No | No | | Story point estimation | No | No | | Attachments | No | No | ## Troubleshooting **"File not found"** Check that the path is correct and the file exists. Relative paths are resolved from the current working directory. **"File appears to be binary"** The file contains a null byte. Ensure it is a plain text `.md` file. **"Missing required markdown environment variable: MARKDOWN_TASKS_DIR"** Set `MARKDOWN_TASKS_DIR` in `.devintern-code/.env` when using `TASK_TRACKER=markdown`. **Status is not updated after the run** The frontmatter must contain a `status:` field. A file without a `status:` field is processed normally but the field is not written. **Branch name looks wrong** Check the `key:` field in the frontmatter. If no `key:` is set, the branch is derived from the filename. Rename the file or add a `key:` field to control it explicitly. --- # Server Automation # Relay (Instant Events) `devintern worker connect` pairs your worker with the DevIntern relay. Source webhooks (GitHub, Linear, Asana, Trello, Azure DevOps, Jira) reach DevIntern's ingest, are stripped down to reference envelopes, and your worker picks them up within seconds instead of waiting for the next poll. No public endpoint on your side, no tunnels. ## What the relay sees (and what it never sees) An envelope is a reference, not a payload: ```json { "source": "github", "eventType": "pr.review_submitted", "repo": "acme/webapp", "repoId": 987654321, "installationId": 12345678, "ref": { "pr": 142 }, "deliveryId": "gh-delivery-uuid", "ts": "2026-07-03T10:00:00Z" } ``` Explicitly excluded: diffs, file contents, ticket bodies, comment text, and credentials of any kind. When your worker receives an envelope, it fetches the real data directly from GitHub or your tracker using your own local credentials, and everything executes on your machine as usual. If the relay is unreachable, nothing breaks: the worker's regular polling keeps running as a fallback, so relay downtime affects latency only, never correctness. ## How authentication works Connect is an interactive step. The default `devintern worker init` flow offers sign-in, registers GitHub plus the active tracker, and stores the workspace's durable pairing under `~/.devintern/`. The relay verifies the session and your automation entitlement, then mints a durable **relay token** (`drt_…`). The worker uses that token for `/v1/status` and `/v1/events` long-polls, so polling survives session rotation, logouts, and password changes. GitHub repository registration is completed through the DevIntern AI GitHub App. The CLI prints a short-lived installation URL and waits while GitHub authorizes the App. The relay verifies that the signed-in GitHub user can access the requested repository through that installation before it records anything. Routing then uses GitHub's immutable installation and repository IDs—not the user-supplied `owner/name` slug. An installation already associated with another DevIntern account cannot be claimed or overwritten. The App's private key never reaches the worker: it fetches referenced PRs/comments and performs GitHub writes with its local `GITHUB_TOKEN`, so customer-owned `GITHUB_APP_ID` credentials are ignored in this relay-backed mode. GitHub connections created before verified pairing was introduced must run `devintern worker connect github` once again. The command verifies every unpaired GitHub repository listed in the fleet workspace. Old local confirmation markers are not treated as completed setup, while an existing live relay route remains usable during the upgrade. `LICENSE_KEY` is still required for the local unattended license gate when you run `devintern worker` (same as polling mode without the relay). It is not the credential the relay data plane accepts. Tracker webhooks never hit your machine. Self-register commands call the tracker API from your laptop with your own API keys, pointing the callback at a private per-customer ingest URL on the relay. ### Signing in over SSH or mosh Browser OAuth redirects to `http://127.0.0.1:/auth/callback` on the machine where the CLI is running. Over SSH/mosh there is no local GUI, and opening the URL on your laptop hits your laptop's loopback — not the remote process. **Preferred (works with mosh):** sign in on a machine with a browser, then copy the session file: ```bash # on your laptop devintern login scp .devintern-code/.auth-session.json user@host:/path/to/project/.devintern-code/ ``` Then re-run `worker init` / `worker connect` on the remote host (it will see you as signed in). **SSH tunnel alternative:** remote login binds a stable callback port (`17865`, or `DEVINTERN_AUTH_CALLBACK_PORT`). From your laptop: ```bash ssh -N -L 17865:127.0.0.1:17865 user@host # open the printed OAuth URL in your laptop browser ``` Mosh cannot forward TCP — use `ssh` for the tunnel, or the session-copy path above. ## Quick Start ```bash # Sign in (interactive connect step) devintern login # Automation license for the worker daemon # Set LICENSE_KEY in the workspace .env (from https://devintern.com/account) # Pair the workspace repositories for central App delivery devintern worker connect # Open each printed GitHub App URL and authorize the requested repository. # The command waits for every verification, then you can run the worker: devintern worker ``` The worker detects the workspace pairing and starts the relay connection automatically alongside its normal polling. Both `worker init` and `worker connect` store relay state under the workspace home. For Linear, Asana, Trello, or Azure DevOps, set that tracker's credentials in the workspace `.env` or a team's `env_file` / inline `[teams.env]`, then run the matching connect command below. Jira needs no extra Jira env for registration: connect prints a private ingest URL for one-time admin webhook setup. ## Commands | Command | Description | | --------------------------------------- | ------------------------------------------------------------------------- | | `devintern worker connect` | Verify every unpaired GitHub repository in the workspace | | `devintern worker connect linear` | Self-register a Linear webhook for Issue events | | `devintern worker connect asana` | Self-register an Asana webhook for task events | | `devintern worker connect trello` | Self-register a Trello webhook for card events | | `devintern worker connect azure-devops` | Self-register work item service hooks | | `devintern worker connect jira` | Print the one-time Jira admin webhook setup with your private ingest URL | | `devintern worker connect status` | Show relay status and workspace repositories still awaiting verification | In a multi-team workspace, `devintern worker connect linear --team growth` selects that team's credential layers. If exactly one team uses the requested tracker, `--team` is optional and the CLI selects it automatically. The flag is invalid for GitHub and status because those targets are workspace-wide. Current tracker envelopes identify their tracker type but not an individual team registration. When more than one team uses the same tracker type—for example, two separate Jira sites—`worker connect jira` refuses registration and those teams continue using their isolated polling loops. The worker also ignores an ambiguous same-tracker task envelope rather than assigning it to the first matching team. GitHub repository events and teams using distinct tracker types are unaffected. Linear deliveries are verified with a signing secret generated on your machine. Asana deliveries are verified with the hook secret from Asana's registration handshake. Trello, Azure DevOps, and Jira deliveries carry no usable signature, so their authentication is the unguessable ingest URL itself: keep it secret, and re-run connect to rotate it. `worker connect` stores the shared pairing under the workspace home. It skips repositories whose immutable GitHub repository IDs are already verified and continues through the remaining repositories if one pairing fails. Tracker connect uses the selected team's credential layers when applicable; otherwise it reads the workspace `.env` with explicit shell variables taking precedence. ## Environment variables ### Always | Variable | Required | Description | | ------------------- | -------- | ---------------------------------------------------------------------------------------------------- | | (signed-in session) | Connect | Run `devintern login` before `worker connect`. Session lives in `.devintern-code/.auth-session.json` | | `LICENSE_KEY` | Worker | Automation license for unattended `devintern worker` runs (local license gate) | | `WORKER_RELAY_URL` | No | Relay base URL override (default: `https://relay.devintern.com`) | ### Per `worker connect` target These are the same credentials you already use for that tracker. Set them in the workspace `.env`, or use `--team ` to compose that team's `env_file` and inline env. GitHub connect uses every unpaired GitHub repository in `workspace.toml`. Jira connect mints the ingest URL and prints admin setup steps without calling the Jira API. | Target | Required env vars | Notes | | -------------- | --------------------------------------------------------------- | --------------------------------------------------------------- | | `github` | (none beyond login + `LICENSE_KEY` for the worker) | Repositories come from `workspace.toml` | | `linear` | `LINEAR_API_KEY` | Creates the Linear webhook pointing at your relay ingest URL | | `asana` | `ASANA_API_TOKEN`, `ASANA_DEFAULT_PROJECT_GID` | Webhook scoped to that project; Asana handshakes with the relay | | `trello` | `TRELLO_API_KEY`, `TRELLO_API_TOKEN`, `TRELLO_DEFAULT_BOARD_ID` | Webhook scoped to that board | | `azure-devops` | `AZURE_DEVOPS_ORG`, `AZURE_DEVOPS_PAT`, `AZURE_DEVOPS_PROJECT` | Creates work item create/update service hooks | | `jira` | (none for registration) | Paste the printed ingest URL into Jira admin webhooks | Running the worker against those trackers still needs the usual `TASK_TRACKER=…` credentials so the agent can fetch ticket bodies locally after an envelope arrives. Markdown tasks are local files and need no relay. ## How events are handled - Reviews submitted on the agent's own PRs are addressed automatically, same as polling mode. - New PR comments are checked for a `@devintern-ai` mention; the same permission gate applies (only users with push access can direct the agent). - Tracker task events re-run your configured `[defaults].task_query` before acting, so "ready" still means whatever your query says. - Every envelope is deduplicated against the worker's local database, so relay delivery and fallback polling never double-run work. ## Availability The relay requires an automation license (solo supporter, team subscription, or legacy server addon). All sources are supported: GitHub (via the DevIntern Relay App), Linear, Asana, Trello, and Azure DevOps self-register with your own credentials, and Jira uses a one-time admin webhook setup. Markdown tasks are local files and need no relay. Every tracker also keeps working with plain polling if you prefer no DevIntern infrastructure at all. --- # GitHub Integration Guide Prefer the [worker](./worker.md) plus the [relay](./relay.md) for PR review feedback: polling is always on, the central DevIntern AI App delivers events in seconds, and only `GITHUB_TOKEN` is needed locally. This page is the advanced path—`devintern webhook serve` with a customer-owned GitHub App—for air-gapped or self-hosted installs that operate their own public GitHub webhook. This guide covers secure deployment of that listener. ## Table of Contents - [Webhook Server Deployment Guide](#webhook-server-deployment-guide) - [Table of Contents](#table-of-contents) - [Overview](#overview) - [Prerequisites](#prerequisites) - [Exposure Options](#exposure-options) - [Option 1: Cloudflare Tunnel (Recommended)](#option-1-cloudflare-tunnel-recommended) - [Option 2: Tailscale Funnel](#option-2-tailscale-funnel) - [Option 3: Reverse Proxy (Caddy/nginx)](#option-3-reverse-proxy-caddynginx) - [Caddy (Automatic HTTPS)](#caddy-automatic-https) - [nginx](#nginx) - [Option 4: Direct Exposure (Not Recommended)](#option-4-direct-exposure-not-recommended) - [Security Layers](#security-layers) - [1. Webhook Signature Verification (Critical)](#1-webhook-signature-verification-critical) - [2. GitHub IP Allowlisting (Recommended)](#2-github-ip-allowlisting-recommended) - [3. Rate Limiting](#3-rate-limiting) - [4. Firewall Rules (OS-level)](#4-firewall-rules-os-level) - [5. TLS/HTTPS (Required by GitHub)](#5-tlshttps-required-by-github) - [GitHub App Configuration](#github-app-configuration) - [Update App Permissions](#update-app-permissions) - [Configure Webhook](#configure-webhook) - [Running the Server](#running-the-server) - [Environment Variables](#environment-variables) - [Start the Server](#start-the-server) - [Systemd Service (Linux)](#systemd-service-linux) - [Monitoring & Troubleshooting](#monitoring--troubleshooting) - [Logs](#logs) - [Health Check](#health-check) - [Test Webhook Delivery](#test-webhook-delivery) - [Common Issues](#common-issues) - [Debug Mode](#debug-mode) - [Security Checklist](#security-checklist) - [Quick Start Summary](#quick-start-summary) ## Overview The webhook server listens for GitHub PR events and automatically runs Agent to address feedback. The architecture looks like: ``` GitHub → [Exposure Layer] → Webhook Server → Agent Harness → Git Push ``` **Key Security Principle**: The webhook server should never be directly exposed to the internet. Always use one of the secure exposure options below. ### What triggers a run The server acts on these events, and in every case it only proceeds when the bot is **`@`-mentioned** (so it never fires on unrelated comments): | You do this | GitHub event | Notes | | ---------------------------------------- | ------------------------------------------- | --------------------------------------------------------------------------------------- | | Submit a review with **Request changes** | `pull_request_review` (`changes_requested`) | Inline diff comments are addressed in one batch | | Submit a review with **Comment** | `pull_request_review` (`commented`) | Same batch flow; e.g. `@bot please tweak these` | | Leave a **top-level PR comment** | `issue_comment` | e.g. `@bot finish implementing this PR` — needs the **Issue comments** event subscribed | Inline review comments are processed _as a batch_ with their parent review — the standalone `pull_request_review_comment` event is intentionally ignored to avoid acting on each line comment separately. Approvals and dismissals are never actionable. ## Prerequisites 1. **Customer-owned GitHub App** (`GITHUB_APP_ID` + private key) configured with webhook permissions. This is required only for this advanced no-relay path: it supplies the bot identity used by the mention gate and custom `slug[bot]` attribution. A personal `GITHUB_TOKEN` can call the API, but by itself it cannot identify the App bot for direct-webhook mentions. See [Configuration](./configuration.md#advanced-customer-owned-github-app). 2. **Webhook Secret** - a random string for request verification 3. **Agent Harness CLI** installed and configured 4. **Git credentials** with push access to target repositories 5. **Automation license** — the webhook server is unattended automation, so it requires a Supporter, Team, or Business key > **License required.** Like scheduled runs, the webhook listener runs unattended and fails the license check without an automation license (Supporter, Team, or Business). Set `LICENSE_KEY` in your `.devintern-code/.env` (or as an `Environment=` entry in the service) to a key from [devintern.com/account](https://devintern.com/account). Generate a webhook secret: ```bash openssl rand -hex 32 ``` You generate this secret yourself — it is **not** issued by GitHub. Put the **same** value in two places: the GitHub App's webhook **Secret** field and `WEBHOOK_SECRET` in your environment. GitHub HMAC-signs each delivery with it, and the server verifies the signature. ## Exposure Options ### Option 1: Cloudflare Tunnel (Recommended) **Zero open ports** - Cloudflare Tunnel creates an outbound-only connection from your server to Cloudflare's edge network. **Pros:** - No inbound ports to open on your firewall - Free tier available - DDoS protection included - Automatic HTTPS - Works behind NAT/firewalls **Setup:** 1. Install cloudflared: ```bash # macOS brew install cloudflared # Linux curl -L --output cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb sudo dpkg -i cloudflared.deb # Or download from https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/ ``` 2. Authenticate: ```bash cloudflared tunnel login ``` 3. Create a tunnel: ```bash cloudflared tunnel create devintern-webhooks ``` 4. Configure DNS (creates webhooks.yourdomain.com): ```bash cloudflared tunnel route dns devintern-webhooks webhooks.yourdomain.com ``` If a DNS record for that hostname already exists (e.g. left over from a previous tunnel), this fails with `code: 1003 … record with that host already exists`. Repoint it at the new tunnel with `--overwrite-dns`: ```bash cloudflared tunnel route dns --overwrite-dns devintern-webhooks webhooks.yourdomain.com ``` 5. Create config file (`~/.cloudflared/config.yml`): ```yaml tunnel: YOUR_TUNNEL_ID credentials-file: /path/to/.cloudflared/YOUR_TUNNEL_ID.json ingress: - hostname: webhooks.yourdomain.com service: http://localhost:3000 - service: http_status:404 ``` 6. Run the tunnel: ```bash # Foreground cloudflared tunnel run devintern-webhooks # Or as a service sudo cloudflared service install sudo systemctl start cloudflared ``` **Final architecture:** ``` GitHub → Cloudflare Edge → Cloudflare Tunnel → localhost:3000 ↓ Webhook Server (no open ports) ``` --- ### Option 2: Tailscale Funnel If you already use Tailscale for your network, Funnel provides a simple way to expose services. **Pros:** - Simple one-command setup - Integrates with existing Tailscale network - Automatic HTTPS with valid certificates **Setup:** 1. Enable Funnel in Tailscale admin console (requires admin access) 2. Start the funnel: ```bash tailscale funnel 3000 ``` 3. Your webhook URL will be: `https://your-machine-name.tailnet-name.ts.net` **Note:** Tailscale Funnel has some limitations on free plans. Check [Tailscale Funnel docs](https://tailscale.com/kb/1223/tailscale-funnel/). --- ### Option 3: Reverse Proxy (Caddy/nginx) Use when you have a server with a public IP and want full control. #### Caddy (Automatic HTTPS) 1. Install Caddy: ```bash # Debian/Ubuntu sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list sudo apt update && sudo apt install caddy # macOS brew install caddy ``` 2. Configure (`/etc/caddy/Caddyfile`): ```caddyfile webhooks.yourdomain.com { reverse_proxy localhost:3000 # Rate limiting rate_limit { zone webhooks { key {remote_host} events 30 window 1m } } # Optional: IP allowlisting for GitHub # See "GitHub IP Ranges" section below for current IPs @blocked not remote_ip 140.82.112.0/20 143.55.64.0/20 185.199.108.0/22 192.30.252.0/22 respond @blocked 403 } ``` 3. Start Caddy: ```bash sudo systemctl enable caddy sudo systemctl start caddy ``` #### nginx 1. Configure (`/etc/nginx/sites-available/webhooks`): ```nginx server { listen 443 ssl http2; server_name webhooks.yourdomain.com; ssl_certificate /etc/letsencrypt/live/webhooks.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/webhooks.yourdomain.com/privkey.pem; # Rate limiting limit_req_zone $binary_remote_addr zone=webhooks:10m rate=30r/m; location / { limit_req zone=webhooks burst=5; # Optional: GitHub IP allowlisting # allow 140.82.112.0/20; # allow 143.55.64.0/20; # allow 185.199.108.0/22; # allow 192.30.252.0/22; # deny all; proxy_pass http://localhost:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` 2. Get SSL certificate: ```bash sudo certbot certonly --nginx -d webhooks.yourdomain.com ``` 3. Enable and start: ```bash sudo ln -s /etc/nginx/sites-available/webhooks /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx ``` --- ### Option 4: Direct Exposure (Not Recommended) **⚠️ Only use this for testing with tools like ngrok** ```bash # ngrok (temporary testing only) ngrok http 3000 ``` Never expose the webhook server directly to the internet in production. --- ## Security Layers Regardless of which exposure method you choose, **always implement these security measures**: ### 1. Webhook Signature Verification (Critical) This is implemented in devintern and **cannot be bypassed**. GitHub signs every webhook with your secret: ```bash # Set your webhook secret export WEBHOOK_SECRET="your-random-secret-here" ``` The server will reject any request without a valid `X-Hub-Signature-256` header. ### 2. GitHub IP Allowlisting (Recommended) GitHub publishes their webhook IP ranges at `https://api.github.com/meta`. Current ranges (may change): ``` 140.82.112.0/20 143.55.64.0/20 185.199.108.0/22 192.30.252.0/22 ``` **Dynamically fetch current IPs:** ```bash curl -s https://api.github.com/meta | jq '.hooks' ``` ### 3. Rate Limiting Prevent abuse even from valid sources. Recommended limits: - 30 requests per minute per IP - Burst of 5 requests ### 4. Firewall Rules (OS-level) If using direct exposure or reverse proxy, add firewall rules: ```bash # UFW (Ubuntu) sudo ufw allow from 140.82.112.0/20 to any port 3000 sudo ufw allow from 143.55.64.0/20 to any port 3000 sudo ufw allow from 185.199.108.0/22 to any port 3000 sudo ufw allow from 192.30.252.0/22 to any port 3000 # iptables iptables -A INPUT -p tcp --dport 3000 -s 140.82.112.0/20 -j ACCEPT iptables -A INPUT -p tcp --dport 3000 -s 143.55.64.0/20 -j ACCEPT iptables -A INPUT -p tcp --dport 3000 -j DROP ``` ### 5. TLS/HTTPS (Required by GitHub) GitHub requires HTTPS for production webhooks. All exposure options above provide automatic HTTPS except direct exposure. --- ## GitHub App Configuration ### Update App Permissions Add these permissions to your GitHub App: 1. Go to your GitHub App settings 2. Navigate to **Permissions & events** 3. Under **Repository permissions**, add: - **Pull request review comments**: Read and write - **Issue comments**: Read and write (for top-level PR comments) - **Actions**: Read (to download failing job logs) - **Commit statuses**: Read (for non-Actions CI providers) 4. Under **Subscribe to events**, enable: - Pull request review - Pull request review comment - Issue comment (lets a user trigger devintern with a top-level PR comment) ### Configure Webhook 1. In your GitHub App settings, go to **Webhooks** 2. Set **Webhook URL** to your server's URL (e.g., `https://webhooks.yourdomain.com/webhooks/github`) 3. Set **Secret** to your `WEBHOOK_SECRET` 4. Select content type: `application/json` 5. Enable events: - Pull request reviews - Pull request review comments --- ## Running the Server ### Environment Variables ```bash # Required export WEBHOOK_SECRET="your-webhook-secret" export LICENSE_KEY="your-automation-license-key" # GitHub authentication. # GitHub App auth is preferred — it resolves the bot identity (slug[bot]), # which is REQUIRED for @mention matching and bot-attributed commits: export GITHUB_APP_ID="123456" export GITHUB_APP_PRIVATE_KEY_PATH="/path/to/key.pem" # OR export GITHUB_APP_PRIVATE_KEY_BASE64="..." # A personal access token works as a fallback when no App is configured: export GITHUB_TOKEN="ghp_..." # Optional export WEBHOOK_PORT="3000" # Default: 3000 export WEBHOOK_HOST="0.0.0.0" # Default: 0.0.0.0 export WEBHOOK_AUTO_REPLY="true" # Reply to addressed comments export WEBHOOK_AUTO_REVIEW="true" # Run self-review loop after addressing feedback export WEBHOOK_AUTO_REVIEW_MAX_ITERATIONS="5" # Max review iterations (default: 5) export WEBHOOK_MAX_RETRIES="3" # Retries per failed webhook job export WEBHOOK_VALIDATE_IP="true" # Reject requests outside GitHub's published IP ranges export WEBHOOK_QUEUE_DB=".devintern-code/queue.db" # Persistent job queue path (default: .devintern-code/queue.db in the project directory) export WEBHOOK_DEBUG="true" # Verbose request/processing logging ``` > **Auth precedence (serve mode):** unlike the CLI — where `GITHUB_TOKEN` takes precedence — the webhook server is **App-first**. When GitHub App credentials are present they are used even if a `GITHUB_TOKEN` is also set, because the App resolves the bot identity needed for `@mention` matching and bot-attributed commits. A token alone still works (the App falls back to it when no App is configured), but mention-gated triggers and `slug[bot]` commit attribution require the App. ### Start the Server `devintern webhook serve` runs the advanced repo-local webhook listener. Keep it separate from the workspace worker so webhook delivery and tracker automation can be operated independently. ```bash # Development bun run src/webhook-server.ts # Production (after build) devintern webhook serve --port 3000 # With PM2 (process manager) pm2 start "devintern webhook serve" --name devintern-webhooks ``` ### Systemd Service (Linux) Create `/etc/systemd/system/devintern-webhooks.service`: ```ini [Unit] Description=@devintern/code Webhook Server After=network.target [Service] Type=simple User=your-user WorkingDirectory=/path/to/your/projects # devintern is a `#!/usr/bin/env bun` script — pin PATH so `bun` resolves # under systemd's minimal environment (check: dirname "$(which bun)"). Environment=PATH=/home/your-user/.local/bin:/home/your-user/.local/share/mise/installs/bun/1.3.2/bin:/usr/local/bin:/usr/bin Environment=LICENSE_KEY=your-automation-license-key Environment=WEBHOOK_SECRET=your-secret Environment=GITHUB_TOKEN=ghp_... Environment=WEBHOOK_AUTO_REPLY=true Environment=WEBHOOK_AUTO_REVIEW=true ExecStart=/usr/local/bin/devintern webhook serve --port 3000 Restart=always RestartSec=10 [Install] WantedBy=multi-user.target ``` Enable and start: ```bash sudo systemctl daemon-reload sudo systemctl enable devintern-webhooks sudo systemctl start devintern-webhooks ``` Rather than putting secrets in the unit file, you can load them from your project's `.env` with `EnvironmentFile=/path/to/.devintern-code/.env` and drop the individual `Environment=` secret lines. **User service (no root):** to run under `systemctl --user` instead, place the unit in `~/.config/systemd/user/`, remove the `User=` line, set `WantedBy=default.target`, manage it with `systemctl --user enable --now devintern-webhooks`, and run `loginctl enable-linger "$USER"` so it survives logout. Pair it with a user-level `cloudflared` unit the same way. **Process cleanup.** While addressing a review, the agent may start long-running processes (dev servers, watchers) to verify its changes. Because the webhook server is always on, those would otherwise accumulate over its lifetime. @devintern/code runs each agent in its own process group and tears that group down as soon as the task finishes, so nothing is left running between reviews. On Linux, the unit's cgroup (default `KillMode=control-group`) also reaps everything when the service stops or restarts, including processes that fully daemonize. --- ## Monitoring & Troubleshooting ### Logs ```bash # Systemd journalctl -u devintern-webhooks -f # PM2 pm2 logs devintern-webhooks ``` ### Health Check The server exposes a health endpoint: ```bash curl https://webhooks.yourdomain.com/health # {"status": "ok", "timestamp": "..."} ``` ### Test Webhook Delivery 1. Go to your GitHub App settings → **Advanced** 2. View **Recent Deliveries** 3. Check response codes and bodies 4. Use **Redeliver** to test ### Common Issues | Issue | Solution | | -------------------------- | ------------------------------------------------------------------------------------------------------------------- | | 401 Unauthorized | Check `WEBHOOK_SECRET` matches GitHub App config | | 403 Forbidden | Check IP allowlisting if enabled | | 500 Internal Error | Check server logs for stack trace | | Timeout | Ensure Agent Harness CLI is installed and working | | No webhook received | Check GitHub App webhook URL and events | | 530 from the public URL | Tunnel up but DNS not routing — the CNAME is missing or points at an old tunnel; re-run `route dns --overwrite-dns` | | `Tunnel not found` in logs | Tunnel was deleted or credentials belong to another account; recreate with `cloudflared tunnel create` | | License check failed | Set `LICENSE_KEY` to a Supporter, Team, or Business automation key | ### Debug Mode Run with verbose logging: ```bash WEBHOOK_DEBUG=true devintern webhook serve ``` --- ## Security Checklist Before going to production: - [ ] Webhook secret is set and matches GitHub App - [ ] HTTPS is enabled (automatic with recommended options) - [ ] Rate limiting is configured - [ ] IP allowlisting is enabled (optional but recommended) - [ ] Server runs as non-root user - [ ] Logs are being collected - [ ] Health monitoring is set up - [ ] Firewall rules are configured (if applicable) --- ## Quick Start Summary **Fastest secure setup (Cloudflare Tunnel):** ```bash # 1. Install cloudflared brew install cloudflared # or appropriate package manager # 2. Create tunnel cloudflared tunnel login cloudflared tunnel create devintern-webhooks cloudflared tunnel route dns devintern-webhooks webhooks.yourdomain.com # (add --overwrite-dns if a record for that host already exists) # 3. Set environment export WEBHOOK_SECRET=$(openssl rand -hex 32) export LICENSE_KEY="your-automation-license-key" export GITHUB_TOKEN="ghp_..." # or GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY_PATH # 4. Start server devintern webhook serve & # 5. Start tunnel cloudflared tunnel run devintern-webhooks # 6. Put the SAME WEBHOOK_SECRET in the GitHub App, and set its webhook URL to: # https://webhooks.yourdomain.com/webhooks/github ``` Your webhook server is now securely exposed with zero open ports! --- # Story Points Estimation Let the [workspace worker](./worker.md) estimate stories on a schedule with `[[estimations]]` in `workspace.toml`, or use the `--estimate` flag as a CLI one-shot. In both modes your AI agent analyzes tasks and automatically assigns story point estimates — useful for backlog grooming, sprint planning, or keeping estimates up to date as tasks evolve. ```toml [[estimations]] id = "weekday-groom" enabled = true cron = "0 9 * * 1-5" query = "status = 'To Do' AND labels IN (NeedsEstimate)" ``` ## What It Does - Analyzes task description, comments, linked resources, and related issues - Produces a **Fibonacci-scale** estimate (1, 2, 3, 5, 8, 13, 21) - Provides **confidence level** (high / medium / low), reasoning, risks, and unclear areas - Sets the story points field directly in JIRA - Posts a rich estimation comment with full context ## Usage (CLI one-shot) `--estimate` remains the interactive/one-shot path; scheduled sweeps run the same engine from the worker. ### Single Task ```bash devintern PROJ-123 --estimate ``` ### Batch via JQL ```bash # Estimate all tasks in the backlog devintern --estimate --query "project = PROJ AND status = 'To Do'" # Estimate unestimated tasks in the current sprint devintern --estimate --query "project = PROJ AND sprint in openSprints() AND 'Story Points' is EMPTY" # Estimate recently updated tasks devintern --estimate --query "project = PROJ AND updated >= -7d" ``` `--jql` still works as a deprecated alias of `--query`. ## Smart Behavior ### Skip Recently Created Tasks Tasks created less than **24 hours ago** are automatically skipped. This gives the team time to refine the description before estimation. ### Smart Re-Estimation If a task already has an estimation comment: - If the task hasn't been updated since the last estimate → **skipped** - If the task was updated after the last estimate → **re-estimated in place** (existing comment is updated, not duplicated) This keeps estimates fresh without creating comment clutter. ## Story Points Scale | Points | Meaning | | ------ | -------------------------------------------------------- | | **1** | Trivial change, config tweak, typo fix | | **2** | Small, well-defined task, single file change | | **3** | Moderate task, a few files, clear requirements | | **5** | Significant feature, multiple files, some complexity | | **8** | Large feature, cross-cutting concerns, integration work | | **13** | Very large, multiple subsystems, high complexity | | **21** | Epic-sized, major architectural change, high uncertainty | ## Configuration ### Story Points Field The tool auto-discovers the story points field in JIRA by searching for common names like: - `Story Points` - `Story Point Estimate` - `Story point estimate` If your JIRA instance uses a custom field name, you can override it in `.devintern-code/settings.json`: ```json { "projects": { "PROJ": { "storyPointsField": "customfield_10016" } } } ``` --- ## Scheduled Estimation with the Worker The [workspace worker](./worker.md) runs unattended estimation for you via `[[estimations]]` in `workspace.toml`. No more cron of `--estimate`: the worker owns the timer, the durable schedule state, and serialization with all other agent work. ```toml [[estimations]] id = "weekday-groom" enabled = true cron = "0 9 * * 1-5" query = "status = 'To Do' AND labels IN (NeedsEstimate)" [[estimations]] id = "sprint-gaps" enabled = true cron = "0 10 * * 3" query = "sprint in openSprints() AND \"Story Points\" is EMPTY" ``` Each entry needs a unique `id`, boolean `enabled`, a non-empty `query`, and exactly one of `cron` or `interval`. There is no `prompt` and no `repo`: a due entry runs one-shot `devintern --estimate --query ""`, which estimates — never implements, branches, or opens PRs. Estimating does not depend on `[defaults].task_query`; an omitted or empty `[[estimations]]` table simply means estimation is off. You can add, remove, enable, disable, reschedule, or change the query of an estimation entry while the worker is running. The worker validates and reconciles the edit automatically without interrupting an in-progress sweep. Notes: - Works with Jira, Linear, Azure DevOps, Asana, and GitHub (comment-only). Trello/markdown workspaces fail at startup. - Runs use the full skip/re-estimate behavior above (24h gate, update-in-place). - Usage-limit aborts end the sweep cleanly; the next occurrence retries. Tickets already written stay done. - Each sweep is visible in the [dashboard](./dashboard.md) under its own `estimate` origin. See [Worker Daemon → Scheduled story-point estimation](./worker.md#scheduled-story-point-estimation) for semantics and troubleshooting. ## Example Output ``` 📊 Running in estimation mode... ============================================================ 📊 Estimating: PROJ-456 🔄 Re-estimating PROJ-456: task updated since last estimate ✅ Estimated PROJ-456: 5 story points (high confidence) ============================================================ 📊 Estimation Summary: Estimated: 3 Skipped (< 24h old): 1 Skipped (not updated): 2 Failed: 0 ``` ## Troubleshooting **"Story points field not found"** - Check your JIRA instance's custom field name for story points - Set `storyPointsField` in `.devintern-code/settings.json` **"Failed to parse estimation response"** - The AI agent may have returned non-JSON output - Try running with `--verbose` to see the raw response - Check that your agent CLI is working correctly **Low confidence estimates** - The estimation comment will flag low confidence and ask for more details - Consider refining the task description before re-estimating --- # PM # Create Jira Issues with @devintern/pm @devintern/pm creates Jira Cloud issues directly from AI-generated stories and tasks. Setup takes a few minutes: you need your site URL, account email, an API token, and a default project key. ## How It Works @devintern/pm uses the [Jira Cloud REST API v3](https://developer.atlassian.com/cloud/jira/platform/rest/v3/) to create issues in a project you configure. - New work items appear as **issues** in the target project with the issue type you select (Story, Task, Bug, Epic, etc.) - Descriptions are converted from markdown to **Atlassian Document Format (ADF)** so headings, lists, and inline formatting render in Jira - Subtasks are created as **Subtask** issue types linked to the parent issue - Epic links set the story's **parent** field to the epic issue key - Issue types are fetched from your Jira project's configuration: pick one that exists in your project This integration targets **Jira Cloud** (`*.atlassian.net`). Jira Data Center / Server is not supported. ## Setup ### 1. Set the backend In your `.devintern-pm/.env`: ```bash TASK_TRACKER=jira JIRA_BASE_URL=https://your-org.atlassian.net JIRA_EMAIL=your-email@example.com JIRA_API_TOKEN=your-api-token JIRA_DEFAULT_PROJECT_KEY=PROJ ``` All four Jira variables are required. `TASK_TRACKER` defaults to `jira` if omitted. ### 2. Find your Jira site URL Your site URL is the hostname you use to open Jira in the browser: ``` https://your-org.atlassian.net/jira/... └─ JIRA_BASE_URL ─┘ ``` Set `JIRA_BASE_URL` to the full URL **without** a trailing slash. `https://` is optional: @devintern/pm strips it automatically. ### 3. Create an API token 1. Go to [Atlassian API tokens](https://id.atlassian.com/manage-profile/security/api-tokens) 2. Click **Create API token**, add a label (e.g. `DevIntern`), and copy the token 3. Paste the token into `JIRA_API_TOKEN` Use the **email address** of the Atlassian account that owns the token for `JIRA_EMAIL`. Authentication is Basic auth (`email:token`). Store credentials in `.devintern-pm/.env`. That file should stay out of version control (`devpm init` adds it to `.gitignore`). ### 4. Set your default project key The project key is the short uppercase prefix on issue keys (e.g. `PROJ` in `PROJ-123`). Find it in: - Any issue URL: `https://your-org.atlassian.net/browse/PROJ-123` → `PROJ` - **Project settings → Details → Key** In interactive mode you can switch projects with **Ctrl+P** before confirming, but `JIRA_DEFAULT_PROJECT_KEY` is still required. ### 5. Create your first issue ```bash devpm --interactive ``` The issue type step lists types configured in your Jira project (Story, Task, Bug, Epic, or custom types from your scheme). ## Issue Types Available types depend on your Jira project and issue type scheme. @devintern/pm fetches non-subtask types from the project API. Pick a type that exists in your project. If you choose a type that is not available (e.g. **Epic** on a project that does not use epics), Jira may reject the request. ## What Gets Created | devpm concept | Jira object | | ------------------------- | ------------------------------------------------ | | Story / Bug / Task / Epic | Issue of the selected type in the target project | | Subtask | Subtask issue linked to the parent issue | | Epic link | Parent field set to the epic issue key | When linking to an epic, enter the epic's issue key (e.g. `PROJ-42` from `.../browse/PROJ-42`). ## Troubleshooting **"Jira backend selected but … configuration is missing"** Set all four variables in `.devintern-pm/.env`: `JIRA_BASE_URL`, `JIRA_EMAIL`, `JIRA_API_TOKEN`, and `JIRA_DEFAULT_PROJECT_KEY`. **"Jira API error (401)"** - API token is invalid or revoked: create a new token at [Atlassian API tokens](https://id.atlassian.com/manage-profile/security/api-tokens) - `JIRA_EMAIL` does not match the account that created the token - Confirm the token was copied without extra spaces **"Jira API error (403)"** - Your account lacks permission to create issues in the target project - Ask a Jira admin to add you to the project with **Create issues** permission **"Jira API error (400)" when creating issues** - The selected issue type is not valid for this project: pick a type from the list in interactive mode - `JIRA_DEFAULT_PROJECT_KEY` is wrong: verify the key in **Project settings → Details** **Epic link fails** - Enter a valid epic issue key (e.g. `PROJ-100`), not a numeric ID - The epic must exist in a project your account can access - Some Jira configurations use **Epic Link** custom fields instead of parent: @devintern/pm sets the **parent** field (works with team-managed projects and modern company-managed epics) **Descriptions show unformatted markdown** Descriptions are converted to ADF before create. If formatting is missing, check that the description is standard markdown (headings, lists, bold): exotic syntax may not map to ADF. **Project picker (Ctrl+P) shows other projects but defaults elsewhere** Issues are created in the project selected in interactive mode, or in `JIRA_DEFAULT_PROJECT_KEY` when not overridden. --- # Create Trello Cards with @devintern/pm @devintern/pm creates Trello cards directly from AI-generated stories and tasks. Setup takes about a minute: you only need to generate an API token. ## How It Works @devintern/pm ships with a built-in Trello Power-Up, so you don't need to register your own app. You just authorize your Trello account once and paste the token into your config. Each story becomes a card. Subtasks become checklist items on the parent card. Issue type selection and epic linking are both skipped: Trello does not expose Jira-style issue types or a native parent hierarchy. ## Setup ### 1. Set the backend In your `.devintern-pm/.env`: ```bash TASK_TRACKER=trello ``` ### 2. Generate your API token Run `devpm`. If `TRELLO_API_TOKEN` is missing, it will print a direct authorization URL. Visit the URL, click **Allow**, and copy the token from the next page. Alternatively, you can generate the token manually at any time: ``` https://trello.com/1/authorize?expiration=never&scope=read,write&response_type=token&name=DevIntern&key=b2d5d1ced28b515c6eb66c40187400b0 ``` > The `expiration=never` parameter creates a long-lived token so you don't need to re-authorize periodically. You can revoke it at any time from your [Trello account settings](https://trello.com/u/me/account). ### 3. Add the token to your config ```bash TRELLO_API_TOKEN=your-generated-token ``` That's it. Run `devpm --interactive` to create your first card. ## Optional Configuration ### Default board and list Without these set, @devintern/pm uses your first board and first list. To pin a specific destination: ```bash TRELLO_DEFAULT_BOARD_ID=abc123 TRELLO_DEFAULT_LIST_NAME="To Do" ``` **Finding your board ID:** Open the board in Trello and look at the URL, `trello.com/b/{boardId}/{board-name}`. The short alphanumeric segment is the ID (e.g. `abc123` in `trello.com/b/abc123/My-Board`). **List name:** Use the exact list title as shown on the board (e.g. `"To Do"`, `"Backlog"`). If the name doesn't match any list on the board, @devintern/pm falls back to the first list. ### Using your own Power-Up If you need isolated rate limits (enterprise use, high-volume automation), register your own Power-Up: 1. Go to [trello.com/power-ups/admin](https://trello.com/power-ups/admin) and create a new Power-Up 2. Navigate to **API Key** tab → **Generate a new API Key** 3. Add to your config: ```bash TRELLO_API_KEY=your-own-api-key TRELLO_API_TOKEN=your-api-token ``` When `TRELLO_API_KEY` is set, it overrides the bundled key. ## What Gets Created | devpm concept | Trello object | | ------------------ | --------------------------------- | | Story / Task / Bug | Card in the target list | | Subtask | Checklist item on the parent card | | Epic link | Not supported (step is skipped) | ## Troubleshooting **"Trello backend requires TRELLO_API_TOKEN"** Visit the authorization URL printed in the error, click Allow, and add the resulting token to your `.devintern-pm/.env`. **"No Trello boards found"** Your token authorized successfully but the account has no boards. Create at least one board in Trello first. **"No lists found on Trello board"** The target board exists but has no lists. Trello boards need at least one list before cards can be created. **API 401 errors after working previously** Your token was revoked. Go to [Trello account settings → Applications](https://trello.com/u/me/account) to check. Re-run the authorization flow to generate a new token. **Rate limit errors (429)** @devintern/pm uses the shared Power-Up key, which allows 300 requests per 10 seconds across all users. For a typical `devpm` run (3–5 API calls), this limit is never reached in practice. If you hit it under heavy automation, switch to your own Power-Up key via `TRELLO_API_KEY`. --- # Create GitHub Issues with @devintern/pm @devintern/pm creates GitHub Issues directly from AI-generated stories and tasks. Setup takes a few minutes: you need a Personal Access Token and a target repository. ## How It Works @devintern/pm uses the [GitHub REST Issues API](https://docs.github.com/en/rest/issues) to create and update issues in a repository you configure. - New issues appear under the repo's **Issues** tab - Stories, bugs, tasks, and epics map to issue labels (see below) - Subtasks become linked issues with a task list on the parent issue - Epic linking is not supported: GitHub Issues has no native parent hierarchy, so the epic linking step is skipped in interactive mode and the `--epic` flag is ignored ## Setup ### 1. Set the backend In your `.devintern-pm/.env`: ```bash TASK_TRACKER=github GITHUB_REPO=your-username-or-org/your-repo ``` `GITHUB_REPO` is the repository in `owner/repo` form (e.g. `acme/my-app`). All issues are created in this repository. ### 2. Create a Personal Access Token Both token types work. **Fine-grained tokens are recommended**: they grant only the permissions @devintern/pm needs on specific repositories. #### Fine-grained (recommended) 1. Go to [Fine-grained tokens → Generate new token](https://github.com/settings/personal-access-tokens/new) 2. Set **Repository access** to include your target repo (and any others you want visible in the Ctrl+P repo picker) 3. Under **Permissions → Repository permissions**, set: - **Issues:** Read and write - **Metadata:** Read (required; selected automatically) 4. Generate the token and copy it For organization-owned repositories, an org admin may need to **approve** the token before it works. #### Classic 1. Go to [Classic tokens → Generate new token](https://github.com/settings/tokens/new) 2. Select scopes: - **Private repositories:** `repo` (full control of private repositories) - **Public repositories only:** `public_repo` 3. Generate the token and copy it Classic tokens with `repo` work but grant broader access than necessary. ### 3. Add the token to your config ```bash GITHUB_TOKEN=ghp_xxxxxxxxxxxx ``` Run `devpm --interactive` to create your first issue. ## Issue Types and Labels When you pick an issue type in @devintern/pm, it applies a GitHub label: | devpm issue type | GitHub label | | ---------------- | ------------- | | Story | `enhancement` | | Bug | `bug` | | Task | `task` | | Epic | `epic` | New repositories include `bug` and `enhancement` by default. Create `task` and `epic` labels in your repo if you use those issue types: otherwise GitHub may reject the request when applying a missing label. ## What Gets Created | devpm concept | GitHub object | | ------------------------- | ------------------------------------------------------------- | | Story / Bug / Task / Epic | Issue with title, body, and mapped label | | Subtask | New issue linked from a `## Subtasks` task list on the parent | | Epic link | Not supported (step is skipped) | ## Troubleshooting **"Missing required environment variables" / GitHub configuration errors** Set both `GITHUB_TOKEN` and `GITHUB_REPO` (as `owner/repo`) in `.devintern-pm/.env`. **"GitHub API error (401)"** - Token is invalid or expired: generate a new one - For fine-grained tokens on org repos, check whether an admin still needs to approve the token **"GitHub API error (403)"** - Token lacks **Issues: Read and write** (fine-grained) or `repo` / `public_repo` (classic) - Token does not have access to the configured repository - Your account lacks permission to create issues in that repo **"GitHub API error (422)" when creating issues** - A mapped label (`task`, `epic`, etc.) does not exist in the repository: create it under **Issues → Labels**, or pick an issue type whose label already exists **Repo picker (Ctrl+P) shows other repos but issues go elsewhere** Issues are always created in the repository set by `GITHUB_REPO`. The picker lists repositories your token can access for reference; changing the selection does not redirect issue creation yet. --- # Create Asana Tasks with @devintern/pm @devintern/pm creates Asana tasks directly from AI-generated stories and tasks. Setup takes a few minutes: you need a Personal Access Token and optionally a default project. ## How It Works @devintern/pm uses the [Asana REST API](https://developers.asana.com/reference/rest-api-reference) to create tasks in a project you configure. - New work items appear as **tasks** in the target project (descriptions use Asana rich text via `html_notes`, not raw markdown in `notes`) - Subtasks are created as Asana subtasks on the parent task - Epic links use Asana's parent-task relationship - Issue type selection is skipped: Asana does not expose Jira-style issue types through this integration ## Setup ### 1. Set the backend In your `.devintern-pm/.env`: ```bash TASK_TRACKER=asana ASANA_API_TOKEN=your-asana-pat ``` ### 2. Create a Personal Access Token 1. Go to the [Asana developer console](https://app.asana.com/0/developer-console) 2. Create a new **Personal access token** 3. Copy the token into `ASANA_API_TOKEN` The token must be able to read projects and create tasks in the workspaces you use. ### 3. (Optional) Pin a default project If you omit `ASANA_DEFAULT_PROJECT_GID`, @devintern/pm uses the first accessible project. To always create tasks in a specific project, set: ```bash ASANA_DEFAULT_PROJECT_GID=2222222222222222 ``` #### Finding your project GID Open the project in Asana and copy the numeric ID that appears right after `/project/` in the URL: ``` https://app.asana.com/1/1111111111111111/project/2222222222222222/list/3333333333333333 └─ project GID ─┘ ``` Use `2222222222222222` as `ASANA_DEFAULT_PROJECT_GID`. The first number is the workspace ID; the last number is a view/section ID. Neither of those is the project GID. ### 4. Create your first task ```bash devpm --interactive ``` ## What Gets Created | devpm concept | Asana object | | ------------------ | ------------------------------------------------------------------------------------------- | | Story / Task / Bug | Task in the target project (markdown from devpm is converted to Asana HTML in `html_notes`) | | Subtask | Subtask under the parent task | | Epic link | Parent task relationship via `setParent` | ## Troubleshooting **"Asana backend selected but ASANA_API_TOKEN is missing"** Set `ASANA_API_TOKEN` in `.devintern-pm/.env` after creating a token in the [developer console](https://app.asana.com/0/developer-console). **"Asana API error (401)"** - Token is invalid or revoked: generate a new PAT - Confirm the token was copied without extra spaces **"Asana API error (403)"** - Your account may lack permission to create tasks in the target project - The project may be in a workspace the token cannot access **Tasks land in the wrong project** - Set `ASANA_DEFAULT_PROJECT_GID` to the numeric ID from that project's Asana URL - In interactive mode, pick the correct project before confirming **"Could not fetch projects" warning in interactive mode** - Check `ASANA_API_TOKEN` and network access to `app.asana.com` - Ensure your Asana account has at least one project; create one in the UI if needed --- # Create GitLab Issues with @devintern/pm @devintern/pm creates GitLab issues directly from AI-generated stories and tasks. Setup takes a few minutes: you need a Personal Access Token and a target project. Both **GitLab Cloud (gitlab.com)** and **self-hosted instances** are supported. ## How It Works @devintern/pm uses the [GitLab REST API v4](https://docs.gitlab.com/ee/api/issues.html) to create and update issues in a project you configure. - New issues appear under the project's **Issues** tab - Stories, bugs, tasks, and epics map to issue labels (see below) - Subtasks become linked issues with a task list on the parent issue - Epic linking is not supported: project issues have no native parent hierarchy, so the epic linking step is skipped in interactive mode and the `--epic` flag is ignored ## Cloud vs. Self-Hosted | Flavor | `GITLAB_BASE_URL` | Notes | | --------------------- | ----------------------------------- | ----------------------------------------------- | | GitLab Cloud | omit (default `https://gitlab.com`) | Works out of the box | | Self-managed instance | e.g. `https://gitlab.example.com` | Protocol is kept; `http://` internal hosts work | ## Setup ### 1. Set the backend In your `.devintern-pm/.env`: ```bash TASK_TRACKER=gitlab # Cloud (default) — omit or leave commented: # GITLAB_BASE_URL=https://gitlab.com # Self-hosted — set your instance root URL: GITLAB_BASE_URL=https://gitlab.example.com ``` `GITLAB_BASE_URL` is the instance root, without `/api/v4`. ### 2. Create a Personal Access Token 1. Sign in to your GitLab instance 2. Go to **User Settings → Access Tokens** (`/-/user_settings/personal_access_tokens`) 3. Create a token with the **`api`** scope (read + write). A read-only setup needs `read_api`, but @devintern/pm creates and updates issues, so `api` is required. 4. Copy the token (tokens starting `glpat-…` cannot be viewed again after creation) Project access tokens and group access tokens also work if they include the `api` scope and at least **Reporter** role on the target project. ### 3. Configure the target project ```bash GITLAB_TOKEN=glpat_xxxxxxxxxxxx GITLAB_PROJECT=group/repo ``` `GITLAB_PROJECT` accepts: - A project path: `group/repo` or with subgroups `group/sub/repo` - A numeric project ID (visible under the project name on the project overview) Run `devpm --interactive` to create your first issue. ## Issue Types and Labels When you pick an issue type in @devintern/pm, it applies a GitLab label: | devpm issue type | GitLab label | | ---------------- | ------------- | | Story | `enhancement` | | Bug | `bug` | | Task | `task` | | Epic | `epic` | New projects do not include all of these labels by default. Create them under **Issues → Labels** in your project, or issue creation may fail when applying a missing label. ## What Gets Created | devpm concept | GitLab object | | ------------------------- | ------------------------------------------------------------- | | Story / Bug / Task / Epic | Issue with title, description, and mapped label | | Subtask | New issue linked from a `## Subtasks` task list on the parent | | Epic link | Not supported (step is skipped) | ## Troubleshooting **"Missing required environment variables"** Set both `GITLAB_TOKEN` and `GITLAB_PROJECT` in `.devintern-pm/.env`. `GITLAB_BASE_URL` is optional for gitlab.com but required for self-hosted instances. **"GitLab API error (401)"** - Token is invalid or expired: generate a new one - On self-hosted instances, confirm the token was created on the same instance as `GITLAB_BASE_URL` **"GitLab API error (403)"** - Token lacks the `api` scope - Your account lacks permission to create issues in that project (need at least Reporter) **"Invalid GITLAB_PROJECT"** Use `group/repo` (subgroups allowed) or a numeric project ID — not the human-readable project name alone. **Self-signed certificates** The integration talks to your instance's normal HTTPS endpoint. Instances behind self-signed TLS need the certificate trusted at the OS level where @devintern/pm runs. --- # Create Linear Issues with @devintern/pm @devintern/pm creates Linear issues directly from AI-generated stories and tasks. Setup takes a few minutes: you need a Personal API key and optionally a default team. ## How It Works @devintern/pm uses the [Linear GraphQL API](https://developers.linear.app/docs/graphql/working-with-the-graphql-api) to create issues in a team you configure. - New work items appear as **issues** in the target team (descriptions are sent as markdown, which Linear renders natively) - Subtasks are created as sub-issues linked to the parent issue - Epic links use Linear's parent-issue relationship - Issue type selection is skipped: Linear does not expose Jira-style issue types through this integration ## Setup ### 1. Set the backend In your `.devintern-pm/.env`: ```bash TASK_TRACKER=linear LINEAR_API_KEY=lin_api_xxxxxxxxxxxx ``` ### 2. Create a Personal API key 1. Go to [Linear API settings](https://linear.app/settings/api) 2. Under **Personal API keys**, click **Create key** 3. Add a label (e.g. `DevIntern`) and copy the key into `LINEAR_API_KEY` The key starts with `lin_api_` and cannot be viewed again after you leave the page. Store it in `.devintern-pm/.env`. That file should stay out of version control (`devpm init` adds it to `.gitignore`). The key inherits your Linear account permissions: you can create issues in any team you belong to. ### 3. (Optional) Pin a default team If you omit `LINEAR_DEFAULT_TEAM_KEY`, @devintern/pm uses the first accessible team. To always create issues in a specific team, set: ```bash LINEAR_DEFAULT_TEAM_KEY=ENG ``` #### Finding your team key The team key is the short prefix before the issue number in identifiers like `ENG-42` or `DES-7`. You can find it in either place: - **From an issue:** open any issue in the team. The identifier prefix is the team key (`ENG` in `ENG-42`) - **From team settings:** Linear → **Settings** → **Teams** → select your team → **Key** In interactive mode you can also pick a team with **Ctrl+P** before confirming. ### 4. Create your first issue ```bash devpm --interactive ``` ## What Gets Created | devpm concept | Linear object | | ------------------ | ---------------------------------------- | | Story / Task / Bug | Issue in the target team | | Subtask | Sub-issue linked to the parent issue | | Epic link | Parent issue relationship via `parentId` | ## Troubleshooting **"Linear backend selected but LINEAR_API_KEY is missing"** Set `LINEAR_API_KEY` in `.devintern-pm/.env` after creating a key at [Linear API settings](https://linear.app/settings/api). **"Linear API error (401)"** - API key is invalid or revoked: create a new Personal API key - Confirm the key was copied without extra spaces or a `Bearer` prefix (paste the raw `lin_api_…` value) **"No Linear teams found"** Your account has no teams yet, or the API key's user cannot see any. Create a team in Linear first, or ask a workspace admin to add you to one. **Issues land in the wrong team** - Set `LINEAR_DEFAULT_TEAM_KEY` to the key from that team's settings or issue identifiers - In interactive mode, pick the correct team with **Ctrl+P** before confirming **"Could not fetch teams" warning in interactive mode** - Check `LINEAR_API_KEY` and network access to `api.linear.app` - Ensure your Linear account belongs to at least one team --- # Create Azure DevOps Work Items with @devintern/pm @devintern/pm creates Azure DevOps work items directly from AI-generated stories and tasks. Setup takes a few minutes: you need a Personal Access Token, your organization slug, and a target project. ## How It Works @devintern/pm uses the [Azure DevOps Work Items REST API](https://learn.microsoft.com/en-us/rest/api/azure/devops/wit/work-items) to create items in a project you configure. - New work items appear in the target **project** with the issue type you select (User Story, Bug, Task, etc.) - Descriptions are converted from markdown to **HTML** for `System.Description` (Azure DevOps defaults to HTML; raw markdown would display as plain text) - Subtasks are created as **Task** work items linked as children of the parent - Epic links use Azure DevOps parent/child hierarchy relationships - Work item IDs are numeric (e.g. `123`): use the ID from the work item URL when linking epics This integration targets **Azure DevOps Services** (`dev.azure.com`). Self-hosted Azure DevOps Server is not supported. ## Setup ### 1. Set the backend In your `.devintern-pm/.env`: ```bash TASK_TRACKER=azure-devops AZURE_DEVOPS_ORG=your-organization AZURE_DEVOPS_PAT=your-pat-token AZURE_DEVOPS_PROJECT=YourProject ``` All three Azure DevOps variables are required. ### 2. Find your organization slug Open your organization in the browser. The slug is the segment after `dev.azure.com/`: ``` https://dev.azure.com/contoso/MyProject/_workitems/edit/123 └─ org ─┘ ``` Set `AZURE_DEVOPS_ORG=contoso`: the slug only, not the full URL. ### 3. Create a Personal Access Token 1. Go to [Personal Access Tokens](https://dev.azure.com/your-org/_usersSettings/tokens) (replace `your-org` with your organization slug) 2. Click **+ New Token** 3. Set a name (e.g. `DevIntern`) and choose an expiration 4. Under **Scopes**, enable: - **Work Items:** Read & write - **Project and Team:** Read 5. Under **Organizations**, select your org (or **All accessible organizations** if you use multiple) 6. Create the token and copy it into `AZURE_DEVOPS_PAT` Store the PAT in `.devintern-pm/.env`. That file should stay out of version control (`devpm init` adds it to `.gitignore`). ### 4. Set your default project `AZURE_DEVOPS_PROJECT` must match the project **name** exactly as shown in Azure DevOps, not the project ID or description. Find it in the URL when you open a board or backlog: ``` https://dev.azure.com/contoso/MyProject/_boards/board/t/... └─ project name ─┘ ``` Or copy the name from **Project settings → Overview**. In interactive mode you can switch projects with **Ctrl+P** before confirming, but `AZURE_DEVOPS_PROJECT` is still required as the default. ### 5. Create your first work item ```bash devpm --interactive ``` The issue type step lists work item types from your project's process template (Agile, Scrum, Basic, CMMI, or a custom process). ## Issue Types Available types depend on your Azure DevOps process template: | Process template | Common types | | ---------------- | ---------------------------------------------- | | Agile | Epic, Feature, User Story, Task, Bug | | Scrum | Epic, Feature, Product Backlog Item, Task, Bug | | Basic | Epic, Issue, Task | | CMMI | Epic, Feature, Requirement, Task, Bug | @devintern/pm fetches the types configured in your project. Pick a type that exists in your process. If you choose a type that is not defined, work item creation will fail. ## What Gets Created | devpm concept | Azure DevOps object | | ------------------------- | ------------------------------------------------------------------------------------- | | Story / Bug / Task / Epic | Work item of the selected type in the target project (markdown → HTML in Description) | | Subtask | **Task** work item linked as a child of the parent | | Epic link | Parent/child hierarchy link between work items | When linking to an epic, enter the parent work item's **numeric ID** (from the URL: `.../_workitems/edit/456` → `456`), not a Jira-style key like `PROJ-123`. ## Troubleshooting **"Azure DevOps backend selected but … configuration is missing"** Set all three variables in `.devintern-pm/.env`: `AZURE_DEVOPS_ORG`, `AZURE_DEVOPS_PAT`, and `AZURE_DEVOPS_PROJECT`. **"Azure DevOps API error (401)"** - PAT is invalid or expired: create a new token - Confirm the token was copied without extra spaces - Verify the token's organization scope includes the org in `AZURE_DEVOPS_ORG` **"Azure DevOps API error (403)"** - PAT lacks **Work Items (Read & write)** scope: regenerate with the correct permissions - Your account may not have permission to create work items in the target project: ask a project admin to grant **Contributors** access or equivalent **"Azure DevOps API error (404)" when creating work items** - `AZURE_DEVOPS_ORG` or `AZURE_DEVOPS_PROJECT` is wrong: check the slug and project name against your Azure DevOps URL - Project name is case-sensitive and must match exactly (including spaces) **Work item creation fails with an invalid work item type** - The selected type does not exist in your process template (e.g. choosing **Story** on a Scrum project that uses **Product Backlog Item**) - Re-run interactively and pick a type from the list, or use `--type` with a valid type name for your project **Descriptions show raw markdown syntax (e.g. `##`, `**bold**`)** Azure DevOps `System.Description` defaults to **HTML** via the REST API. @devintern/pm converts markdown to HTML before creating the work item. If you still see unrendered markdown on older work items, those were likely created before this conversion was added. Azure DevOps also supports an opt-in **Markdown** format (`/multilineFieldsFormat/System.Description` = `Markdown`) on organizations with the New Boards markdown editor. @devintern/pm uses HTML for compatibility with the default format. **Subtask creation fails** - Subtasks are always created as **Task** work items. If your process template does not include Task (unusual), subtask creation will fail - Confirm the parent key is a numeric work item ID that exists in the project **Epic link fails with "Work item not found"** - Azure DevOps uses numeric IDs, not Jira-style keys: enter the epic's work item ID (e.g. `456` from `.../_workitems/edit/456`) - Both the story and epic must exist in a project your PAT can access **Project picker (Ctrl+P) shows other projects but defaults elsewhere** Work items are created in the project selected in interactive mode, or in `AZURE_DEVOPS_PROJECT` when not overridden. Ensure the default project name matches the project you intend to use. --- # Chat Bot Alpha (devpm serve) > **Alpha:** The chat bot is experimental and may not work properly. Expect bugs and breaking changes, and do not rely on it for critical workflows yet. `devpm serve` runs a chat bot where your team already talks. Mention the bot with a rough idea, get an AI-drafted story back in a thread, refine it in plain language, and approve it to file the task in your tracker (Jira, Linear, Trello, Azure DevOps, Asana, GitHub Issues, or Markdown). The bot runs entirely on your machine. It connects outbound to Slack (Socket Mode) and Telegram (long polling), so you need no public URL, no webhook setup, and no hosted service. Bot tokens, tracker credentials, and the AI agent all stay local. ## How it works 1. Mention the bot with an idea: `@devpm users should be able to reset their password` (Slack), or just DM your bot on Telegram. On Slack you can also use `/devpm `. 2. The bot replies in a thread with a draft: a title, a description with acceptance criteria, and Create / Split into subtasks buttons. 3. Reply in the thread to refine it: "make the acceptance criteria stricter", "add a rollout plan". Each reply regenerates the draft in place. 4. Optional thread commands: - `type Bug` or `type Story` changes the issue type - `project PROJ` targets a different project - `split` decomposes the story into subtasks - `help` shows a reminder of these commands 5. Approve it: reply `create`, react with a checkmark, or press the Create button. The bot files the task (and any subtasks) and posts the link. Each thread holds one draft. After the task is created, mention the bot again to start a new one. Idle drafts expire after 24 hours (configurable via `DEVPM_CHAT_SESSION_TTL_HOURS`). ## Setup Connect at least one platform from your project directory: ```bash devpm connect telegram # paste a BotFather token, ~2 minutes devpm connect slack # guided app creation from a pre-filled manifest ``` See the [Telegram integration](./telegram-integration.md) and [Slack integration](./slack-integration.md) guides for the full walkthroughs. Then start the daemon: ```bash devpm serve ``` The daemon needs the same setup as the CLI: a configured tracker and an installed AI agent (see [Configuration](./configuration.md)). Use `--platform slack` or `--platform telegram` to run a single platform when both are configured. ## Running it long-term `devpm serve` is a plain foreground process. Run it under your preferred supervisor: ```bash # tmux tmux new -s devpm 'devpm serve' ``` ```ini # systemd (~/.config/systemd/user/devpm.service) [Unit] Description=devpm chat bot [Service] WorkingDirectory=/path/to/your/project ExecStart=devpm serve Restart=on-failure [Install] WantedBy=default.target ``` Draft sessions and thread subscriptions persist to `.devintern-pm/`, so an in-progress draft survives a restart: reply in its thread and the bot picks it back up. ## Security notes - Bot tokens live in `.devintern-pm/.env` next to your tracker credentials and never leave your machine. - The bot connects outbound only. There is no inbound port, no public endpoint, and no DevIntern-hosted relay involved. - Message content is sent to your configured AI agent the same way `devpm --prompt` is, using your own agent CLI and keys. ## Troubleshooting - "No chat platform configured": run `devpm connect telegram` or `devpm connect slack` first. - The bot connects but ignores channel messages on Slack: invite it to the channel, and start conversations by mentioning it. - Drafts feel slow: generation runs your local AI agent, and the bot processes one agent run at a time. The progress message updates while it works. --- # Telegram Integration (Alpha) > **Alpha:** The Telegram chat bot is experimental and may not work properly. Expect bugs and breaking changes, and do not rely on it for critical workflows yet. Draft and file tracker tasks by messaging a Telegram bot. Setup takes about two minutes and needs no server, no public URL, and no webhook: the [devpm chat bot](./chat-bot.md) long-polls Telegram from your machine. ## Create the bot 1. Open Telegram and message [@BotFather](https://t.me/BotFather) 2. Send `/newbot` and follow the prompts (pick a display name and a username) 3. BotFather replies with an HTTP API token like `110201543:AAHdqTcv...` ## Connect it From your project directory: ```bash devpm connect telegram ``` Paste the token when prompted. The command validates it against the Telegram API and saves it as `TELEGRAM_BOT_TOKEN` in `.devintern-pm/.env`. The token stays on your machine. ## Use it ```bash devpm serve ``` Then DM your bot a rough idea: > users should be able to reset their password The bot replies with a drafted story. Reply to refine it, `type Bug` to change the issue type, `split` for subtasks, and `create` (or a checkmark reaction) to file it. The bot answers with the task link. ## Group chats The bot works best in DMs and in forum-style groups with topics. In plain groups, Telegram has no real threads, so keep one draft going at a time and reply directly to the bot's messages. ## Troubleshooting - Token rejected: regenerate it with BotFather (`/token`) and run `devpm connect telegram` again. - Bot does not respond in a group: make sure it is a member, and note that group privacy mode may hide messages that do not mention it. Disable privacy mode via BotFather (`/setprivacy`) or mention the bot explicitly. --- # Slack Integration (Alpha) > **Alpha:** The Slack chat bot is experimental and may not work properly. Expect bugs and breaking changes, and do not rely on it for critical workflows yet. Draft and file tracker tasks without leaving Slack. Your team mentions the bot with a rough idea, refines the AI draft in a thread, and approves it to create the task in Jira, Linear, or any other supported tracker. The bot uses Slack's Socket Mode, so the [devpm chat bot](./chat-bot.md) connects outbound from your machine: no public URL, no request signing setup, and no app review. You create your own Slack app from a pre-filled manifest, so all tokens stay in your workspace and your `.devintern-pm/.env`. ## Create the app From your project directory: ```bash devpm connect slack ``` The command prints a link that opens Slack's "create an app from manifest" page with everything pre-configured: the bot user, the `/devpm` slash command, Socket Mode, and the required scopes (`app_mentions:read`, `chat:write`, `commands`, `reactions:read`, `reactions:write`, plus channel, group, and DM history). 1. Open the printed link, pick your workspace, and click Create 2. Install the app: Settings, then Install App, then Install to Workspace 3. Copy the Bot User OAuth Token (starts with `xoxb-`) 4. Generate an app-level token: Settings, then Basic Information, then App-Level Tokens, with the `connections:write` scope (starts with `xapp-`) 5. Paste both tokens back into the `devpm connect slack` prompt The command validates both tokens against the Slack API and saves them as `SLACK_BOT_TOKEN` and `SLACK_APP_TOKEN` in `.devintern-pm/.env`. ## Use it ```bash devpm serve ``` Invite the bot to a channel, then either: - Mention it: `@devpm users should be able to reset their password` - Or use the slash command: `/devpm users should be able to reset their password` The bot posts a drafted story in a thread with Create and Split into subtasks buttons. Reply in the thread to refine the draft, `type Bug` to change the issue type, `project PROJ` to retarget it, and approve with the Create button, a `create` reply, or a checkmark reaction. The bot posts the task link when it is filed. ## Troubleshooting - Bot token rejected: reinstall the app to the workspace and copy the fresh `xoxb-` token. - App-level token rejected: the `xapp-` token must have the `connections:write` scope. - No response to channel messages: the bot must be invited to the channel, and new conversations start with a mention or `/devpm`. --- # Automation # @devintern/code Quick Start **@devintern/code** is your AI intern for automatically implementing tasks. Point it at a local markdown file or a ticket in your task tracker, and it runs your AI agent, creates a feature branch, commits the changes, and can open a pull request. Supported task trackers today: **Jira** (default), **Linear**, **Trello**, **Asana**, **Azure DevOps**, **GitHub Issues**, and **local markdown files** (no PM account required). ## Prerequisites - **[Bun](https://bun.sh) runtime** (required to run @devintern/code) - A local markdown file, or a task tracker account with API access (Jira, Linear, Trello, Asana, Azure DevOps, or GitHub Issues) - AI agent CLI installed (e.g., Claude Code, OpenCode, Codex, Cursor) - Git repository for your project ## Installation Install globally with Bun: ```bash # Install Bun if not already installed curl -fsSL https://bun.sh/install | bash # Install @devintern/code globally bun install -g @getdevintern/code ``` ## Initialize Configuration Navigate to your project directory and run: ```bash devintern init ``` In a terminal, this starts an interactive setup wizard that: - Detects an existing @devintern/pm configuration (`.devintern-pm/.env`) in the same project and offers to reuse those tracker credentials, so you skip straight to validation - Asks which task tracker you use — local **markdown files** lead the menu as the zero-account way to try DevIntern in minutes, followed by Jira, Linear, GitHub Issues, Azure DevOps, Asana, and Trello - Links you directly to the provider's token creation page and prompts for each credential, with a pointer to the matching setup guide in these docs - Validates the connection with a real API call before finishing (you can retry, edit values, or skip) - Offers an optional GitHub token for pull request creation - Detects installed AI agent CLIs (and warns with install steps when none are found) - Offers to sign in to DevIntern on the spot (`devintern login` equivalent) - Finishes with a readiness checklist so your first run cannot fail on something setup could have caught - Writes your answers to `.devintern-code/.env`, creates `settings.json` for per-project configuration, and adds a whitelist block to your `.gitignore` (`.devintern-code/*` plus `!settings.json` and `!.env.example` exceptions) so credentials and local run state never get committed, while `settings.json` and the `.env.example` template stay trackable Not ready to connect a real task tracker yet? Choose **Markdown files** in the wizard. It configures a local task directory (`./tasks` by default), requires no tracker account or API credentials, and lets you switch to Jira, Linear, or another tracker later. See [Markdown File Tasks](./markdown-tasks.md) for the supported file format and workflow. For scripted or CI setups, pass `--yes` (or `--no-interactive`) to skip the prompts and write a commented configuration template instead: ```bash devintern init --yes ``` ### Re-running init on an existing setup Running `devintern init` in an already-configured project no longer refuses — it offers a short menu: **update** your current tracker's credentials (stored values become Enter-to-keep defaults), **switch** to a different tracker (your GitHub PR token carries over), or exit without changes. Updates are merged into `.env`, so comments, custom variables, and previously-skipped optionals are preserved. ## Connect Your Task Tracker The wizard handles credentials for you. If you skip `init`, running a task in an unconfigured project from an interactive terminal offers to launch the guided setup inline before failing. If you used `--yes`, or want to change trackers later, edit `.devintern-code/.env` for the tracker you use. Optionally edit `.devintern-code/settings.json` for status or list transitions after a run. | Tracker | When to use | Setup guide | | ------------------ | --------------------------------------------------------- | ----------------------------------------------------------- | | **Jira** (default) | Jira Cloud issues, JQL batch runs, story point estimation | [Jira Integration](./jira-integration.md) | | **Linear** | Linear issues by ID or URL, IssueFilter batch runs | [Linear Integration](./linear-integration.md) | | **Trello** | Trello cards by short link or URL | [Trello Integration](./trello-integration.md) | | **Asana** | Asana tasks with project section transitions | [Asana Integration](./asana-integration.md) | | **Azure DevOps** | Azure DevOps work items by ID or URL | [Azure DevOps Integration](./azure-devops-integration.md) | | **GitHub Issues** | GitHub issues with status labels, PRs in the same repo | [GitHub Issues Integration](./github-issues-integration.md) | | **Markdown files** | Local `.md` specs, no PM account needed | [Markdown File Tasks](./markdown-tasks.md) | **Jira:** add `JIRA_BASE_URL`, `JIRA_EMAIL`, and `JIRA_API_TOKEN`. You do not need to set `TASK_TRACKER` (it defaults to `jira`). **Trello:** set `TASK_TRACKER=trello` plus `TRELLO_API_KEY` and `TRELLO_API_TOKEN`. Shared options (GitHub/Bitbucket PRs, agent harness, output directory) are covered in [Configuration](./configuration.md). ## Run Your First Task Not sure everything is wired up? Run `devintern doctor` for a readiness check (agent CLI, tracker credentials, sign-in) with a fix hint per issue. If you chose Markdown during setup, add `TASK-1.md` to your configured task directory and pass its filename without the extension: ```bash devintern TASK-1 ``` For a connected tracker, pass its task reference instead: **Jira** ```bash devintern PROJ-123 --create-pr ``` **Trello** (`TASK_TRACKER=trello` in `.devintern-code/.env`) ```bash devintern AbCdEf12 --create-pr ``` ## What's Next? Turn the successful one-off run into a worker that watches for ready tasks and opens pull requests without you starting each run: ```bash devintern worker init devintern worker ``` - [Set up the worker](./worker.md): automate task pickup, pull request feedback, and recurring work - [CLI reference](./usage.md): run individual tasks and queries on demand - [Tracker setup guides](#connect-your-task-tracker): open the guide for the tracker you selected during setup --- # Create Tasks # @devintern/pm Quick Start **@devintern/pm** automates story and task creation across multiple project management tools with AI. Transform Figma designs, error logs, or requirements into well-structured issues in seconds. For the primary visual workflow, [download DevIntern PM](https://devintern.com/pm-desktop/). The desktop app checks on launch that **Git** and **at least one supported agent CLI** are on your PATH (including common GUI-launch locations). If something is missing, install it and choose **Check again**. Continue here if you prefer to work in the terminal. ## Prerequisites - **[Node.js](https://nodejs.org) 20 or newer**: Required to run @devintern/pm ([Bun](https://bun.sh) works too) - **Git**: Required for project folders, GitHub connect, and update-from-remote - AI agent CLI installed and configured (e.g., Claude Code, OpenCode, Codex, Cursor) - Account with at least one supported PM tool (Jira, Linear, Trello, Azure DevOps, Asana, or GitHub) - **For Figma functionality**: [Figma MCP server](https://developers.figma.com/docs/figma-mcp-server/remote-server-installation/) must be installed and configured in your AI agent (Claude Code only) ## Installation Install globally with npm: ```bash npm install -g @getdevintern/pm ``` Or with Bun: ```bash bun install -g @getdevintern/pm ``` ## Initialize Configuration Navigate to your project directory and run: ```bash devpm init ``` In a terminal, this starts an interactive setup wizard that: - Detects an existing @devintern/code configuration (`.devintern-code/.env`) in the same project and offers to reuse those tracker credentials, so you skip straight to validation - Asks which tracker you use (Jira, Linear, Trello, Azure DevOps, Asana, GitHub Issues, GitLab, or markdown files) - Links you directly to the provider's token creation page and prompts for each credential, with a pointer to the matching setup guide in these docs - Validates the connection with a real API call before finishing (you can retry, edit values, or skip) - Writes your answers to `.devintern-pm/.env` and updates your `.gitignore` to exclude `.devintern-pm/.env` (to prevent leaking secrets) If you need to create credentials manually, use the setup guide for your tracker: [Jira](./jira-integration.md), [Linear](./linear-integration.md), [Trello](./trello-integration.md), [Asana](./asana-integration.md), [Azure DevOps](./azure-devops-integration.md), [GitHub Issues](./github-integration.md), or [GitLab](./gitlab-integration.md). For scripted or CI setups, pass `--yes` (or `--no-interactive`) to skip the prompts and write the configuration template instead. The non-interactive path also migrates matching values from `.devintern-code/.env` if present: ```bash devpm init --yes ``` ## First Run The interactive mode provides a step-by-step terminal UI for creating tasks. This is the recommended way to use @devintern/pm: ```bash devpm --interactive ``` You can also pick a different agent up front: ```bash devpm --interactive --harness opencode ``` In interactive mode, press `Ctrl+G` at any step to switch among installed agent harnesses without restarting. The interactive mode will guide you through: 1. **Source type selection**: Choose between Figma URL, error log, or free-form prompt 2. **Source input**: Enter your Figma URL, error log, or requirements 3. **Custom instructions** (optional): Add additional requirements or focus areas 4. **Epic linking** (optional): Link to an existing Jira epic 5. **Issue type** (Jira, Azure DevOps, GitHub, Markdown only): Select Task, Story, Bug, Epic, or enter a custom type. Task is the default; press Enter to accept it. This step is skipped for Linear, Trello, and Asana, which do not support setting an issue type. 6. **Prompt style**: Choose between PM style or Technical style 7. **Confirmation**: Review your configuration before proceeding ## What's Next? - [Configure your PM backend](./configuration.md) - [Learn CLI usage patterns](./usage.md) - [Create tasks from Slack or Telegram](./chat-bot.md) --- # Automation # Worker Daemon `devintern worker` runs devintern as a single long-running workspace daemon on your own machine. It acquires events for every configured repository and executes them locally. Your code, credentials, and agent execution never leave your machine. ## Quick Start The fastest way to set up the worker is the guided setup: ```bash devintern worker init devintern worker ``` `worker init` reuses tracker config from `devintern init` (or runs that subset if missing), writes a 1-repo [workspace](./workspaces.md), validates and stores the ready-tasks query, checks any automation license (Supporter or Team/Business), offers zero-port relay setup plus the central DevIntern App, and can generate a native user service for Linux or macOS. Polling provides fallback acquisition when the relay is unavailable. The repo-local direct webhook server is an advanced, separate service and is not part of this wizard. In the standard path, install the central [DevIntern AI App](https://github.com/apps/devintern-ai/installations/new) on the repositories in your workspace. Its private key stays on DevIntern infrastructure and events arrive as reference-only relay envelopes. Your local `GITHUB_TOKEN` fetches PR data, checks permissions, replies, and creates PRs. `worker init` registers every GitHub repository already listed in `workspace.toml`; after adding repositories, `devintern worker connect` verifies every workspace repo still awaiting pairing. If the relay is intentionally unavailable, the wizard does not offer the hosted App. Polling and the worker's own PRs still work with `GITHUB_TOKEN`; air-gapped mention handling uses the advanced customer-owned App setup in [GitHub authentication](./configuration.md#advanced-customer-owned-github-app). Or configure by hand and start directly: ```bash # After a workspace exists (`worker init`, or `worker scaffold` + `worker add-repo`) devintern worker # Advanced: run the repo-local GitHub webhook listener separately devintern webhook serve ``` ## Agent failover Set `AGENT_HARNESS=codex,grok` (comma-separated, priority first) in the workspace `.env` so the worker keeps going when one agent hits a usage limit. Failover applies to every worker job: tracker tasks, PR review addressing, `@mention` runs, conflict resolution, scheduled automations, estimations, dashboard retries, and relay-driven work. Details: [Failover across multiple harnesses](./configuration.md#failover-across-multiple-harnesses). ## Error-monitor auto-fixes `[[error_monitors]]` entries let the worker turn unresolved production errors into normal repo-scoped fix runs. Each Sentry project maps explicitly to one `[[repos]]` entry and can inherit an optional `[[teams]]` environment, so one worker can safely serve multiple teams, repositories, and credentials. See [Sentry Auto-fixes](./sentry-integration.md) for the schema and setup. ## Recurring automations Put recurring work in `workspace.toml`. Set `repo` when the workspace has multiple repositories; it is optional for a one-repo workspace: ```toml [[automations]] id = "dependency-health" enabled = true repo = "web-app" interval = "6h" prompt = """Pick one outdated dependency and upgrade it within the same major version. Run the test suite; if anything breaks, revert the upgrade instead of fixing forward.""" [[automations]] id = "flaky-test-triage" enabled = true repo = "web-app" cron = "0 9 * * 1" prompt = """Re-run the test suite twice and look for flaky tests. For each flaky test, add a short comment explaining the suspected race condition. Do not change production code.""" ``` Every entry needs a stable unique `id`, boolean `enabled`, non-empty `prompt`, and exactly one schedule. Intervals use positive minutes, hours, or days (`15m`, `6h`, `1d`). Cron expressions have five fields and use the worker host's timezone in v1; persisted occurrence times are UTC. Configuration is validated on load; while the worker runs it revalidates edits to `workspace.toml` automatically (SIGHUP forces a reload) — see [Workspaces → Editing workspace.toml while running](./workspaces.md#editing-workspace.toml-while-running). Automations are a valid event source, so `devintern worker` stays running without a task query when at least one automation entry is configured (disabled entries are validated but not scheduled). ### What an automation is Automations are independent of your task tracker: **the prompt is the task**. Each occurrence writes the prompt to a local markdown task file and feeds it through exactly the same pipeline as any other task — clarity check, planning, implementation, commit, PR creation, auto-review, run records. Nothing is created in your tracker, so no tracker credentials are needed for automation-only workers. Concretely, each occurrence: 1. Writes `~/.devintern/automations//.md` (or the equivalent under `DEVINTERN_WORKSPACE_DIR`). 2. Spawns the normal CLI on that file as a subprocess, so the run gets its own branch, commits, and — by default — a pull request. 3. Records the attempt with the `scheduled` origin and the automation id, so you can filter scheduled runs in the [dashboard](./dashboard.md). Because the occurrence is just a markdown task, you can reproduce or rerun any occurrence by hand: ```bash devintern ~/.devintern/automations/dependency-health/2026-08-24T09-00-00-000Z.md ``` You usually don't have to: the [dashboard](./dashboard.md#run-an-automation-now) has a **Run now** action per automation that executes the prompt immediately through this same pipeline and records the attempt with the `manual` origin, so new or edited configurations can be validated in seconds instead of waiting for the next schedule window. ### Writing good prompts The prompt replaces the ticket description the agent would normally read, so treat it like you would write a task for a new teammate: - **Scope it to one change per run.** "Apply one safe improvement" produces reviewable PRs; "clean up the repository" produces sprawling ones. - **State the guardrails.** What not to touch, when to stop, what must pass (`Run the test suite before committing`). - **Say what done means.** The pipeline's incomplete-detection reads the agent output; concrete success criteria make escalations rare. - Prefer recurring maintenance work (dependency bumps within a major, flaky-test triage, changelog refreshes, TODO sweeps) over open-ended feature work. ### Tuning how occurrences run Occurrences use the same flag defaults as polled tasks: `[defaults].worker_task_args` in `workspace.toml` (default `--create-pr`). For example, set `worker_task_args = "--create-pr --auto-review"` to have every automated PR go through the review loop too. This setting applies to polled tracker tasks as well. ### Schedule semantics Schedule cursors and claims live in `queue.db`. Missed occurrences coalesce to at most one immediate run after startup. The occurrence cursor advances atomically when claimed, so a crash does not replay a possibly completed run. Active claims receive heartbeats; after two minutes without a heartbeat a later due occurrence may recover the stale claim. If the same automation is still active at its next occurrence, that occurrence is logged and skipped without creating a run record. If the repository lock is held by another task, the occurrence is also skipped. This is an at-most-once policy: skipped occurrences are not replayed. A dashboard-triggered manual run holds the same lease while it is active, so a scheduled occurrence coming due mid-validation is skipped rather than run concurrently. On shutdown the scheduler stops its timer, terminates active automation subprocess groups, waits for them to exit, and leaves their claims recoverable in SQLite. ### Troubleshooting | Symptom | Likely cause | | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | No occurrences fire after editing the TOML | Check the worker log: the reload logs validation errors naming the offending entry, and changing a schedule resets its cursor (the next run is the next scheduled time, not immediately). | | `occurrence skipped: previous run is active` | The previous occurrence still runs (or its lease is stale). Long prompts may simply need a longer schedule. | | `occurrence skipped: repository is busy` | Another task holds the repo run lock; the next occurrence will retry. | | Scheduled runs missing from the dashboard | Filter the run list by origin `scheduled`; check the worker has an automation license (startup log). | | Task files pile up under `~/.devintern/automations/` | They are small and safe to delete — they are only run inputs; the durable record is the run history in `queue.db`. | | A run failed and you need to know why | Open the dashboard's Logs tab to read recent worker output without a shell on the machine ([details](./dashboard.md)). | | The dashboard Logs tab is empty | The daemon tees its output to `worker.stdout.log` / `worker.stderr.log` in the workspace home. Check those files or `journalctl --user -u devintern-worker`. | ## Scheduled story-point estimation `[[estimations]]` in `workspace.toml` runs unattended `--estimate` sweeps on a schedule. Entries use the same schedule grammar as `[[automations]]`, but the body is a **query**, not a prompt — there is no implementation, no branch, no worktree, no PR, and no `repo` key even in multi-repo workspaces: ```toml [[estimations]] id = "weekday-groom" enabled = true cron = "0 9 * * 1-5" query = "status = 'To Do' AND labels IN (NeedsEstimate)" [[estimations]] id = "sprint-gaps" enabled = true cron = "0 10 * * 3" query = "sprint in openSprints() AND \"Story Points\" is EMPTY" ``` Each entry needs a unique `id`, boolean `enabled`, non-empty `query`, and exactly one of `cron` or `interval`. Omitting the table (or leaving every entry disabled) changes nothing: estimation is simply off, and `[defaults].task_query` is never estimated as a side effect. The workspace tracker must support estimation (Jira, Linear, Azure DevOps, Asana, GitHub comment-only); Trello/markdown workspaces fail at startup with a clear error. `worker init` does not ask about estimations — add tables by hand. Estimation entries live-reload with `workspace.toml`: added and re-enabled entries schedule their next future occurrence, changed schedules reset their cursor, query edits apply to the next sweep, and removed entries stop scheduling without interrupting a sweep already in progress. When an entry comes due the worker runs one-shot `devintern --estimate --query ""` from the workspace home. That path keeps all of the interactive behavior: tickets younger than 24 hours are skipped, already-estimated tickets are skipped unless the ticket changed since the estimate, changed tickets are re-estimated with the estimate comment updated in place, points are written to the tracker field, and usage-limit aborts exit cleanly so the next occurrence retries. Each sweep is recorded with its own `estimate` origin (plus the schedule id) in the [dashboard](./dashboard.md) — it never shows up as a scheduled implement run. ### Serialization Agent usage limits are account-global. While scheduled estimation is configured, every agent run in the worker process — implement tasks, automation occurrences, PR reviews, conflict resolutions, and estimation sweeps — takes turns through one process-level gate, so at most one agent subprocess burns quota at a time. Estimation shares the [automation scheduler](#schedule-semantics): durable cursors, leases, missed-occurrence coalescing, and overlap skipping live under `estimation:` keys in `queue.db`. ```bash # The CLI one-shot stays available for ad-hoc estimates: devintern --estimate --query "project = PROJ AND status = 'To Do'" ``` ## Polling mode With `[defaults].task_query` in `workspace.toml`, the worker polls your tracker on an interval (`[defaults].poll_interval`, default 60 seconds) and runs every task that matches the query. The query uses the same language as batch `--query` runs for your tracker, so "ready" means whatever your query says, for example a status or label. How a poll cycle works: 1. A cheap change detector asks the tracker "did anything change since the last cursor?" and nothing else. 2. Only when something changed, the worker re-runs your query to get the tasks that are actually ready. 3. Each ready task is picked up once per change: the worker remembers the task's last seen update stamp, so a task re-enters only when it is updated again. 4. Tasks run one at a time through the normal pipeline (branch, implementation, PR, tracker updates), with `[defaults].worker_task_args` controlling the flags (default `--create-pr`). Cursors persist in `.devintern-code/queue.db`; after a restart the worker resumes where it left off instead of starting from "now". Polling is available for all seven trackers: Jira, Linear, GitHub Issues, Azure DevOps, Asana, Trello, and markdown. Trello polling uses the board actions feed and requires `TRELLO_DEFAULT_BOARD_ID`; Asana polling uses the Events API and requires `ASANA_DEFAULT_PROJECT_GID`. A folder of markdown tasks is the fastest way to try the worker: no tracker account needed. Point `[defaults].tracker` at `markdown`, set `MARKDOWN_TASKS_DIR` in the workspace `.env`, and put the ready-tasks filter in `[defaults].task_query`. ### Re-running a task When a run cannot finish, devintern posts an "Implementation Incomplete" comment on the ticket (crash, interrupt, and failed-feasibility comments do the same) and moves it back to your to-do status. That comment tells you how to unlock a retry. The next pickup is gated so an unchanged ticket is not retried in a loop; you unlock a retry by changing the ticket: - **Edit the description** with more detail, or - **Post any comment** on the ticket (a one-line clarification is enough), or - **Delete the bot's failure comment** from the ticket. The gate ignores the automation's own comments: posting the failure comment (and moving the ticket back) updates the tracker's timestamp, but neither of those counts as a change, so a failed ticket sits still until a human edits it. This applies to every failure kind — incomplete implementations as well as crash, interrupt, and usage-limit comments. Any of those actions bumps the ticket's update stamp, so the worker picks it up on the next change detection and the retry runs. On a retry the agent is told which attempt this is, why the previous attempt stopped, and which comments are new since then, so new guidance takes priority. Each attempt gets its own branch (`feature/{key}`, then `feature/{key}-attempt-2`, and so on). If a run completes but you want a different result, move the ticket back to your to-do status (optionally with a comment describing what to change) and it re-runs the same way. Retry bookkeeping lives in `.devintern-code/queue.db` next to the worker's cursors. For local one-off runs, `devintern TASK-123 --force` re-runs a task even if nothing on the ticket changed; do not put `--force` in `[defaults].worker_task_args`, since that would disable the gate for every polled task. ### Interrupted runs are recovered on startup A graceful stop (Ctrl-C, `SIGTERM`) comments on an in-flight ticket and moves it back to To Do. A hard crash — power cut, kernel panic, `kill -9`, a laptop that died — skips that cleanup, which used to leave the ticket stranded in "In Progress". The worker now bridges that gap on startup. Before any new tickets are acquired, it detects task runs left `in_progress` by the previous (dead) worker instance and gives each affected ticket the same treatment a graceful shutdown would have: the processing-failure comment is posted (explaining that the worker exited unexpectedly before a pull request could be created, and how to unlock a retry), and the ticket is moved back to your To Do status so it is no longer stranded. Each recovery is logged. Recovery respects the retry gate: the requeued ticket is not re-run on restart (no duplicate execution). It sits in To Do like any other ticket whose last attempt failed, and runs again when the ticket changes — an edited description, a new comment, or `--force`. Two guards keep the recovery from making noise or causing harm: - **Tickets that moved on are left alone.** If a ticket is no longer in your configured In Progress status (someone closed it, moved it to review, or otherwise handled it after the crash), the worker does not comment on or move it. - **Very old orphans are not announced.** Runs started more than 7 days ago are marked failed in the database but produce no comment or transition, on the assumption they were already handled manually. Set `WORKER_ORPHAN_MAX_AGE_HOURS` in the workspace `.env` to change the cutoff (`0` disables the feedback for every orphan). Runs from scheduled automations and PR reviews are not part of this: automations recover through their own claim machinery (the next occurrence picks up after a stale lease), and PR runs have their own comment flows. ### Ticket matches the query but is not picked up The worker log is the diagnostic. Look for `[poll:]` (for Jira, `[poll:jira]`): - `📌 picking up KEY` — it was claimed on this tick. - `⏳ KEY deferred; will retry next poll` — the target repository was busy, so the task was not attempted and its claim remains pending automatically. - `⏭️ skipping KEY (already processed at this update)` — this ticket was already claimed at this version. Edit or comment on it so its update stamp changes, then wait for the next change detection. - `have no update stamp from the tracker` — search results are missing `updated`, so the worker cannot tell versions apart and will not retry after the first attempt. Restarting the worker does not help; a one-off `devintern KEY` still runs the ticket by hand. - No tracker pickup/skip lines at all — nothing has changed since the last cursor in `.devintern-code/queue.db`. A ticket last edited before that cursor is not re-evaluated until something on the tracker updates. ## Working windows (quiet hours) The drain of ready tasks can be limited to wall-clock windows — nights only is the classic case — using `[worker.schedule]` in `workspace.toml`: ```toml [worker.schedule] active = ["22:00-06:00"] # pickup allowed only inside these daily windows blocked = [] # subtract from active windows; wins on conflict timezone = "" # optional IANA name; blank = machine local time catch_up_missed = true # one catch-up drain if a whole window elapsed unused ``` Windows are wall-clock per day, may cross midnight (`start` greater than `end`), union when multiple are set, and resolve overlaps toward staying quiet (`blocked` always wins). Only **new-task pickup** pauses: review replies, @mentions, recurring automations, and relay events run normally, and any task already picked up finishes even after its window closes. Timezone and DST details, missed-window catch-up, and status surfaces (startup banner, one-log-line-per-flip, dashboard strip) are described in [Working windows](./automated-task-processing.md#working-windows-quiet-hours). To force an immediate drain without touching the schedule, run `devintern worker run-now`. ## Options The daemon itself takes almost no flags. Durable settings live in `workspace.toml`: ```toml [workspace] dashboard = true # false disables the embedded dashboard dashboard_port = 4400 # optional; default 4400 ci_failure_fix = false # opt in to automatic CI repair on agent PRs [defaults] task_query = "status=todo" worker_task_args = "--create-pr" poll_interval = 60 ``` | Option | Description | | -------------------- | ----------------------------------------------------------------- | | `--workspace ` | Use this `workspace.toml` (default `~/.devintern/workspace.toml`) | | `-v, --verbose` | Verbose logging | Unattended automation is exactly where sandboxing the agent matters most: set `AGENT_SANDBOX=auto` in the workspace `.env` to confine agent runs to the project workspace. See [Sandboxing the Agent](./configuration.md#sandboxing-the-agent) for providers and setup. ## Review feedback on the agent's PRs In polling mode the worker also watches the pull requests it created (no webhook needed). When a human requests changes or leaves new inline review comments on one of the agent's own PRs, the worker addresses the feedback automatically; no mention is required on its own PRs. Closed and merged PRs leave the watch list on their own: the watch list is reconciled with GitHub on every poll cycle, so PRs merged or closed outside the worker (and PRs that disappear because a repository was renamed, transferred, or deleted) drop out of the open count within one poll. The watch list is scoped to repos listed in `workspace.toml`. Registry entries for any other repo — typically left behind when a repository is renamed or transferred — are unwatched automatically at startup instead of being polled (and failing auth) forever. The regular polling requests use ETags, and GitHub does not count `304 Not Modified` responses against the API rate limit. The worker makes unconditional PR requests only once to hydrate state after startup and immediately before an eligible base-sync attempt. Comparison results are reused for each immutable base/head SHA pair. ### Merge conflicts on the agent's PRs When a watched PR falls behind its base branch, the worker catches the branch up automatically whether the base merges cleanly or conflicts. Eligibility comes from GitHub's own `mergeable_state` (`dirty` = conflicts, `behind` = mergeable but not up to date) — not from ancestry checks against the API-reported base SHA, a field GitHub can leave stale for days. The worker merges the base branch into the PR branch and, only when needed, asks the agent to resolve conflicted files (checking for semantic breakage, not just markers) before the merge is committed; every conflicting PR in the watch list is synced, not just one per tick. The result is pushed normally, never force-pushed: if a human moved the branch in the meantime, the push is rejected instead of being overwritten. A comment on the PR reports successful clean merges and conflict resolutions, and stacked PRs benefit the most, since merging one PR routinely advances the next PR's base. Success is verified, not assumed: after a push the resolver re-fetches the PR (a bounded window of a few seconds) and only reports success once GitHub shows the merge commit and no longer reports conflicts. If GitHub still says `dirty` after that window — usually because the base advanced again mid-run — the run counts as failed so another sync opens, and if the push itself cannot be published, every failure path posts a comment on the PR explaining what is missing, so automation never dies silently while the local work looked done. Transient push problems recover on their own: a rejection caused by concurrent forward movement (someone pushed to the branch while the agent was resolving) triggers a bounded refresh-and-retry that rebuilds the merge on top of the new commits, keeping the eventual push a fast-forward. Rollbacks or history rewrites are left alone and deferred instead. This applies only to the agent's own PRs (the same watch list as review polling). Each base/head SHA pair is a durable event in `.devintern-code/queue.db`; new commits on the PR branch open a fresh event, so an exhausted attempt is retried after the next push. Failures retry up to `WEBHOOK_MAX_RETRIES` (default 3), including across worker restarts, waiting out an exponential backoff between attempts (30s after the first failure, doubling up to 10 minutes) so persistent failures do not hammer the API on every poll tick. Before acting, the worker requires the PR head SHA to remain unchanged for `WORKER_BASE_SYNC_QUIET_SECONDS` (default 30) and then re-fetches both SHAs. Recent or concurrent pushes defer the run without consuming an attempt; if a run defers several times in a row, the event is given up until the head or base moves again. GitHub's PR API can report an outdated `base.sha` for a while, so the resolver always merges the actual fetched tip of the base branch rather than trusting that field. Each resolve run is bounded by `WORKER_RESOLVE_TIMEOUT_SECONDS` (default 1800; `0` disables) — a hung resolver subprocess is killed and counted as a failed attempt, and runs left `in_progress` by a crashed or killed worker are marked failed at the next startup. The same merge logic is available manually for any PR via `devintern resolve-conflicts `; manual runs exit non-zero with a clear message when the fix could not be published. #### Scheduled conflict resolution By default resolution runs as soon as a conflict is detected. Because each resolution is an agent run (and therefore token spend), workspaces on metered AI plans can batch it off-peak instead with `[workspace].conflict_resolution` in `workspace.toml`: ```toml [workspace] conflict_resolution = "scheduled" conflict_resolution_cron = "0 3 * * *" # or conflict_resolution_interval = "1d" ``` In scheduled mode the poller still detects every conflict on the first tick it appears and queues it durably (a pending base-sync event), but the agent is not invoked. When the scheduled window arrives — cron uses the worker host timezone, intervals are relative — the worker resolves all queued conflicts in one pass and logs the active mode and next window at startup. The window stays open for a grace period (`WORKER_RESOLVE_WINDOW_GRACE_MINUTES`, default 60) so quiet-period waits and retry backoffs inside the pass can still complete; anything unresolved when it closes waits for the next window. A window that arrives while the worker is down (missed nightly run) catches up on the first tick after restart. Stale resolutions cannot happen: before invoking the agent the worker re-fetches the PR, and closed/merged PRs or a conflict that resolved itself are dropped from the queue without spending tokens. The manual `devintern resolve-conflicts ` command always works on demand, and once GitHub reports a PR conflict-free its queued event never triggers an agent run. The setting applies to the whole workspace and live-reloads with `workspace.toml`. Between windows a conflicted PR cannot be merged, so teams that rely on instant rebases should keep `auto`. See [Workspaces → Automatic conflict resolution](./workspaces.md#automatic-conflict-resolution-auto-vs-scheduled-vs-disabled) for the config reference and tradeoffs. To turn automatic conflict resolution off entirely — no detection, no queuing, no agent runs — set `conflict_resolution = "disabled"`: conflicted PRs stay conflicted until resolved by hand or via `devintern resolve-conflicts `. ## CI failures on the agent's PRs Set `[workspace].ci_failure_fix = true` to watch GitHub Actions and commit statuses on every open PR the worker created and ask the agent to repair failures. The switch is off by default because each repair spends agent tokens and can push a commit. It live-reloads with `workspace.toml`. The watch is continuous while the worker and PR remain open, not just when the PR is created. It runs once at worker startup and then every `[defaults].poll_interval` seconds, survives restarts through the workspace database, and stops when the PR closes, its repository leaves the workspace, or the setting is disabled. Only PRs recorded in the local `agent_prs` registry are watched; similarly named PRs created elsewhere are not discovered automatically. Only completed `failure` and `timed_out` workflow runs, plus failed legacy commit statuses, trigger repair. The worker waits while any workflow is pending before declaring CI green, deduplicates successful repair runs by head SHA and workflow-run or status ID, and retries failed/no-op invocations up to `CI_FIX_MAX_ATTEMPTS` (default 3). After exhaustion it comments on the PR and waits for a human push or a green result before resetting the budget. Failing Actions job logs are reduced to an error-focused excerpt. Pending, failing, and not-yet-reported CI is checked at the configured workspace poll interval. Once a PR's CI is terminal green and remains unchanged, the watcher progressively backs off that PR to 5, 15, and then 30 minutes. An observed PR or CI change returns it to the configured interval, and a worker restart performs an immediate reconciliation. The worker continues checking green PRs while they remain open so delayed reruns and newly added workflows are still detected. Relay-backed workspaces perform these API calls with the local `GITHUB_TOKEN`. A fine-grained token—or the customer-owned App used by a no-relay worker—needs **Actions: Read** and **Commit statuses: Read** in addition to the normal PR and contents permissions. Existing App installations must be re-approved after adding permissions. No extra webhook event subscription is required because CI is polled. GitHub does not currently expose its separate Checks permission for fine-grained PATs, so check-run-only CI providers are not watched unless they also publish a commit status; GitHub Actions is fully supported through the Actions API. ## Mention the bot on any PR The worker also reacts to mentions on pull requests it did not create. When a teammate writes a comment like `@devintern address the review feedback` on any PR in the repository, the worker picks it up on the next poll and handles it through the same pipeline. Detection is a repository-wide sweep of new comments (two requests per interval, regardless of how many PRs are open), so mentions work without any webhook setup. Guardrails apply before the agent acts: - Only users with push access (write, maintain, or admin) can direct the agent. Mentions from read-only users and non-collaborators are ignored, and the check fails closed on errors. - Fork PRs are skipped with an explanatory comment unless the PR allows maintainer edits. - The worker never force-pushes; if a human pushed to the branch meanwhile, the push is rejected instead of overwriting. - Mentions posted before the worker first started are not dug up. In the standard setup, pair the workspace with the relay and install the central DevIntern AI App. The worker recognizes `@devintern-ai` and uses your local `GITHUB_TOKEN` to fetch the comment, enforce the permission gate, and perform changes. No `GITHUB_APP_ID` or private key is stored locally. Without the relay, configure a customer-owned App for mention identity; see [GitHub authentication](./configuration.md#advanced-customer-owned-github-app). ## How events are handled - Events are persisted to a local SQLite queue (`.devintern-code/queue.db`) before processing, so a crash or restart never loses accepted work. - Runs interrupted by a dead worker are recovered on startup: their tickets get the failure comment and move back to To Do before new work is picked up. - Duplicate webhook deliveries are detected by GitHub's delivery id and skipped. - Review feedback is processed before new task pickup: a human waiting on feedback beats a ticket that can wait a minute. - One task or scheduled automation runs at a time per repository. ## Instant events with the relay Polling reacts within one interval (about a minute). On its default path, `worker init` offers to sign in and pair the workspace with the [DevIntern relay](./relay.md), including GitHub and the active tracker. Events then reach the worker within seconds as reference envelopes (never code or comment content). Multi-team polling uses isolated clients and cursors; use `worker connect --team ` when one team owns that tracker type. Multiple teams using the same tracker type remain polling-only until relay envelopes carry team registration identity. While relay long-polls are healthy, review and mention acquisition yields to relay and runs only a 30-minute safety sweep; PR lifecycle and conflict reconciliation continue at the normal polling interval. If relay delivery stops, normal feedback polling resumes after a short grace period. Events from different acquisition paths for the same PR are serialized and collapsed into one follow-up check, so fallback coverage cannot start overlapping agent runs. Run `worker connect` after adding repositories or to add or rotate tracker registrations. ## Seeing what the worker did Every run is recorded stage by stage in the local database. The worker serves the [observability dashboard](./dashboard.md) on the loopback-only address `http://localhost:4400` by default; set `[workspace].dashboard = false` to disable it, or `[workspace].dashboard_port` to change the port. You can also run `devintern dashboard` standalone at any time (it works with the worker stopped too). If the dashboard port is unavailable, the worker logs a warning and continues processing. ## Running as a service The worker runs identically on a laptop, VM, or container. `devintern worker init` can write a user-level systemd unit on Linux or a launchd agent on macOS into the workspace home, then prints explicit installation commands. It never installs or starts the service without you running those commands. Running `devintern worker` in a terminal remains fully supported. For pm2 and tunnel setups (advanced webhook mode), see the [GitHub Integration guide](./github-integration.md). If you want the resident daemon idle during parts of the day, configure [working windows (quiet hours)](#working-windows-quiet-hours) instead of wrapping the CLI in cron. ## License The worker is unattended automation and requires an automation license (Supporter, Team, or Business). Interactive runs stay free under the FSL license. --- # Worker Dashboard The dashboard answers a simple question: **what has the worker been doing?** It gives you a local view of current and past runs without making you read terminal output or inspect the worker database. It starts automatically with `devintern worker`. Open [http://localhost:4400](http://localhost:4400) on the machine running the worker. ## What you can do - See whether the worker is running, waiting for its next working window, or processing a task - Follow each run from task pickup through implementation and pull request creation - Open the task or pull request behind a run - Understand why a run failed and retry it after fixing the cause - Run a recurring automation immediately instead of waiting for its next scheduled time - Review recent worker logs when something needs attention The overview also summarizes activity and success rates, which is useful for spotting repeated failures without checking every run individually. ## Retry a failed run Open the failed run and choose **Retry**. The worker queues a fresh attempt using the same task and workspace routing rules. Retry after correcting the underlying problem, for example missing credentials, unclear task details, or a temporary agent failure. The dashboard prevents duplicate retries while another attempt is already queued or running. ## Run an automation now Open **Automations**, find the automation, and choose **Run now**. The worker runs it through the same pipeline as a scheduled occurrence, including its normal repository, environment, and overlap protection. This is useful for testing a new automation prompt or running routine work early. ## View the dashboard without the worker You can inspect existing run history even when the worker is stopped: ```bash devintern dashboard ``` The command reads the local history without changing it. Use `--port ` if port 4400 is already occupied. ## Local by design The dashboard and its data stay on the worker machine. It listens only on a loopback address, so it is not exposed to your network or the public internet. Run history lives in the worker's workspace, alongside its other local state. The dashboard is included with the same automation license or active trial required by the worker. To disable the dashboard or change its port, edit the workspace settings: ```toml [workspace] dashboard = false # dashboard_port = 4400 ``` --- # Workspaces (Multi-Repo Fleet) Workspace mode lets one `devintern worker` process serve every repository you automate. Instead of one worker per repo, you describe repositories once in `~/.devintern/workspace.toml`, then use either one default tracker query or several isolated team tracker sources. The shortest path is `devintern worker init` inside a checkout: that writes a 1-repo workspace (add + `[defaults].task_query`) and you add more repos later with `devintern worker add-repo`. Workspace mode runs under the same automation license as the rest of the worker: any Supporter, Team, or Business key (or an active trial) covers it — one license spans all of your own repos in the fleet. ## How it works - Without `[[teams]]`, the worker polls `[defaults].task_query`. With teams, it creates one isolated tracker client, query, cursor, and dedupe scope per team. - A team can set `repo` for a fixed destination. A team spanning repositories omits `repo` and uses routing rules. A task runs only when its applicable rules agree on one repository; unmatched or ambiguous work is recorded rather than guessed. **A 1-repo workspace needs no routing rules** — N=1 already implies the only checkout (`devintern worker init` starts this way). - The worker manages a bare clone of each repository under `~/.devintern/repos/` and runs every task in a fresh, disposable worktree under `~/.devintern/worktrees/`. Your own checkouts are never touched. Worktrees are removed after a successful run, kept for debugging when a run fails, and swept after `worktrees_ttl_days` — at worker startup and then hourly while the worker runs. - All worker state (queue, cursors, agent PR registry, run records, routing skips) lives in one database at `~/.devintern/state/queue.db`. - Runs are serialized: one task at a time, with a per-repository lock. One systemd unit (or one terminal) drives the whole fleet. ## workspace.toml ```toml [workspace] worktrees_ttl_days = 7 dashboard = true # dashboard_port = 4400 # Batch automatic conflict resolution off-peak instead of instant (default "auto"): # conflict_resolution = "scheduled" # conflict_resolution_cron = "0 3 * * *" # worker host timezone # conflict_resolution_interval = "1d" # exactly one of cron / interval # Or turn it off entirely: conflict_resolution = "disabled" [defaults] tracker = "jira" task_query = "sprint in openSprints() AND labels = devintern" worker_task_args = "--create-pr" poll_interval = 60 default_branch = "main" # pr_labels = ["devintern", "auto-pr"] [[repos]] name = "backend" remote = "git@github.com:acme/backend.git" default_branch = "main" # pr_labels = ["backend"] # env_file = "env/backend.env" # optional, relative to ~/.devintern [repos.env] # optional per-repo overrides GITHUB_REPO = "acme/backend" [[repos]] name = "frontend" remote = "git@github.com:acme/frontend.git" [[routing.rules]] repo = "backend" project = "BACK" [[routing.rules]] repo = "frontend" project = "WEB" labels = ["frontend"] [worker.schedule] active = ["22:00-06:00"] # optional quiet hours: drain new tasks only at night blocked = [] # subtract from active windows (conflicts resolve to quiet) timezone = "" # blank = worker machine's local time catch_up_missed = true [[automations]] id = "backend-maintenance" enabled = true interval = "6h" repo = "backend" prompt = "Inspect the backend and implement one safe maintenance improvement." [[automations]] id = "weekly-frontend-cleanup" enabled = true cron = "0 9 * * 1" repo = "frontend" prompt = "Review the frontend and clean up one source of recurring noise." ``` - `[defaults].tracker` picks the tracker for the single-source fleet query; any tracker with polling support works (Jira, Linear, GitHub Issues, GitLab Issues, Azure DevOps, Asana, Trello, Markdown). - `pr_labels` applies labels to every PR the fleet creates (GitHub only). A repo's `pr_labels` overrides `[defaults].pr_labels`. Outside a workspace, single-repo users get the same behavior by setting `PR_LABELS` (comma-separated) in `.devintern-code/.env`. - Repo names must be unique and filesystem-safe; they become directory names under `repos/` and `worktrees/`. - Rule criteria combine with AND; list values (`components`, `labels`) match when the task carries any of them. Comparisons are case-insensitive. `project` matches the task key prefix for `PROJ-123` style keys (Jira, Linear); trackers with numeric or opaque ids route via labels or components. - `[worker.schedule]` gates only new-task pickup: multiple windows union, windows may cross midnight, `blocked` wins on overlap, and a missed whole window triggers one catch-up drain at startup. Timezone/DST semantics and `devintern worker run-now` are covered in [Running the Worker Unattended: Working windows](./automated-task-processing.md#working-windows-quiet-hours). - `[[automations]]` uses the same schema as single-repo `.devintern-code/automations.toml`. An entry must name `repo` when the workspace has more than one repository. See [Worker Daemon → Recurring automations](./worker.md#recurring-automations) for prompt-writing guidance and schedule semantics. - `[[estimations]]` schedules unattended story-point sweeps (tracker query + cron/interval, no `prompt`, no `repo`). The workspace tracker must support estimation. See [Worker Daemon → Scheduled story-point estimation](./worker.md#scheduled-story-point-estimation). - `[[error_monitors]]` maps each Sentry project to one repo and an optional team, with per-source credential layers for multi-project setups. See [Sentry Auto-fixes](./sentry-integration.md). ### Multiple teams and tracker boards Use `[[teams]]` when one worker must poll separate boards, tracker accounts, or tracker products. Each team has a stable name, tracker, query, and optional credential layers: ```toml [[teams]] name = "platform" tracker = "jira" task_query = "project = PLAT AND labels = devintern" repo = "api" env_file = "env/platform.env" [[teams]] name = "growth" tracker = "linear" task_query = '{"team":{"key":{"eq":"GROW"}}}' repo = "web" [teams.env] LINEAR_API_KEY = "lin_api_..." ``` `repo` is a fixed mapping: every task acquired from that team runs in that repository, regardless of task labels or key shape. This is the simplest and safest setup when a tracker board belongs to one codebase. The named repository must exist in `[[repos]]`. A fixed team cannot also have team-scoped routing rules, because two competing routing models would make precedence unclear. When one team owns several repositories, omit `repo` and add rules scoped to the team: ```toml [[teams]] name = "platform" tracker = "jira" task_query = "project in (PLAT, SRE) AND labels = devintern" env_file = "env/platform.env" [[repos]] name = "api" remote = "git@github.com:acme/api.git" [[repos]] name = "infra" remote = "git@github.com:acme/infra.git" [[routing.rules]] team = "platform" repo = "api" project = "PLAT" [[routing.rules]] team = "platform" repo = "infra" project = "SRE" [[routing.rules]] repo = "infra" labels = ["infrastructure"] # unscoped: available to every non-fixed team ``` Team routing follows these rules: - Rules naming another team are invisible. Rules naming the acquiring team and rules without `team` are applicable. - Set criteria are ANDed; lists are any-of. If applicable matches disagree on the repository, the task is recorded as ambiguous and not run. - An unfixed team in a multi-repo workspace must have at least one applicable rule. Tasks that match none are recorded as unrouted. - Fixed teams ignore unscoped routing rules and always use their configured `repo`. - In a one-repo workspace, omitting both `team.repo` and routing rules still selects the only repository. Credentials layer as workspace `.env` < team `env_file` < inline `[teams.env]` for tracker clients. Task subprocesses retain repository settings and then apply the acquiring team's credential layers, with `TASK_TRACKER` pinned to that team's tracker so comments and transitions go back to the correct board. Team cursor keys use `tracker:team` (for example `jira:platform`), so separate boards of the same tracker never share polling cursors or dedupe records. `[defaults].tracker` and `[defaults].task_query` are optional fallbacks for team entries. Once any `[[teams]]` exist, there is no separate defaults poller. Scheduled estimations still use `[defaults].tracker`; configure it explicitly when using `[[estimations]]`. Team `task_query` and `repo` changes live-reload along with routing rules. Team names, tracker types, `env_file`, and inline credentials are startup-only because changing them requires rebuilding tracker clients and detectors; restart the worker after changing those fields. Tracker relay envelopes currently identify the tracker type, not an individual team registration. Instant tracker relay is therefore enabled only when one workspace team uses that tracker type. If two teams use Jira (or any same tracker), polling remains fully isolated and supported, but `worker connect jira` refuses the ambiguous relay registration and task envelopes for that tracker fail closed to polling. GitHub repository relay remains unaffected. ### Automatic conflict resolution: `auto` vs `scheduled` vs `disabled` When a watched PR conflicts with its base branch, the worker normally resolves it right away (`conflict_resolution = "auto"`, the default — no behavior change on upgrade). Every resolution hands the conflicted files to the AI agent, which consumes tokens — even at 3am when nobody is reviewing the PR anyway. Set `conflict_resolution = "scheduled"` to batch those resolutions into an off-peak window. Polling still detects every conflict immediately and queues it (the PR stays conflicted until then, and the worker logs which mode is active at startup); the agent only runs inside the window: ```toml [workspace] conflict_resolution = "scheduled" conflict_resolution_cron = "0 3 * * *" # or conflict_resolution_interval = "1d" ``` The schedule uses the same format as `[[automations]]`: a five-field cron expression (worker host timezone) or a positive `15m`/`6h`/`1d` interval — exactly one of the two. Exactly one window pass runs per occurrence; if the worker is down when the window arrives (a missed nightly run), the queued conflicts resolve on the first tick after restart. Inside a window the usual safety rules still apply: failed attempts wait out their retry backoff, PRs whose head is still moving wait out the quiet period, and anything not finished before the window closes (60 minutes by default, `WORKER_RESOLVE_WINDOW_GRACE_MINUTES`) waits for the next one. PRs merged upstream before the window opens are skipped — the worker re-checks GitHub's mergeability before invoking the agent. Two things are never delayed by scheduled mode: review feedback on the agent's PRs is addressed immediately as usual, and you can always run `devintern resolve-conflicts ` by hand to fix one PR without waiting for the window — once GitHub reports the PR conflict-free, the queued event never triggers an agent run. The setting is workspace-wide (per-repo overrides are not supported in v1) and live-reloads with the rest of the runtime configuration. The tradeoff to keep in mind: between windows a conflicted PR cannot be merged, so on fast-moving branches where an instant rebase unblocks a waiting reviewer, `auto` stays the better choice. See [Worker Daemon → Merge conflicts on the agent's PRs](./worker.md#merge-conflicts-on-the-agents-prs) for how resolution itself works. Set `conflict_resolution = "disabled"` to turn automatic conflict resolution off entirely: the worker stops watching for conflicts on the agent's PRs altogether — no detection, no queuing, no agent runs. A PR that conflicts with its base simply stays conflicted until someone resolves it (by hand, or on demand via `devintern resolve-conflicts `). Review feedback and @mention handling are unaffected. This is a valid choice when the team prefers to rebase manually, or when the agent is not trusted to resolve conflicts in a sensitive repository. ### How workspace automations differ from single-repo ones The scheduling is identical; only where the work runs changes: - Each occurrence runs in the repo's persistent base worktree (`~/.devintern/worktrees//base`) with the same layered environment as review work: shared `.env` → repo `env_file` → `[repos.env]`. - It takes the normal per-repo run lock, so it never mutates a checkout concurrently with a task or PR run. - Occurrence task files land under the workspace home (`~/.devintern/automations//`), next to `repos/`, `worktrees/`, and the central database — not inside the repo worktrees. ## Creating a workspace ```bash devintern worker scaffold # scaffold ~/.devintern/workspace.toml and .env cd ~/code/backend devintern worker add-repo # add this repo to the workspace cd ~/code/frontend devintern worker add-repo ``` `worker add-repo` reads the repo's origin remote and its `.devintern-code/.env`: - The remote becomes a `[[repos]]` entry (name derived from the remote, unique and filesystem-safe; default branch from `origin/HEAD` when it differs from the workspace default). - Env keys the workspace does not have yet are merged into the shared `.env`. Values that conflict with the workspace `.env` are kept repo-local in that repo's `[repos.env]`; nothing is silently overwritten. - When the repo's env carries a default project key (Jira or Linear), a starter routing rule is seeded for it. - Re-running `add-repo` for the same repo is a no-op. Hand-written comments in `workspace.toml` are preserved; new entries are appended. - `.devintern-code/settings.json` needs no migration: it travels with the repo and applies inside each task worktree. ## Environment Secrets live in one shared `~/.devintern/.env` (tracker credentials, `GITHUB_TOKEN`, agent settings). Advanced no-relay installations may also keep customer-owned GitHub App credentials there. Each repo can layer more on top: 1. Shared workspace `.env` 2. The repo's `env_file` (if set) 3. Inline `[repos.env]` values (highest precedence) For GitHub remotes the worker fills `GITHUB_REPO` automatically from the remote URL. ## Running ```bash devintern worker # auto-detects ~/.devintern/workspace.toml devintern worker --workspace /path/to/workspace.toml ``` The single-source fleet query comes from `[defaults].task_query`; multi-team workspaces use each team's `task_query`. A workspace with automations, estimations, or an enabled error monitor can omit the defaults query. Poll interval, per-task flags, and the embedded dashboard are also set in `workspace.toml` (`poll_interval`, `worker_task_args`, `[worker.schedule]` quiet hours, `[workspace].dashboard` / `dashboard_port`). Direct webhooks are an advanced repo-local service: run `devintern webhook serve` from that repository as a separate process. Automation and estimation schedule state and leases, plus task-polling and error-monitor deduplication state, live in the central workspace database. While the daemon is running you can request one immediate drain (for example while quiet hours are closed) with `devintern worker run-now`; see [Working windows](./automated-task-processing.md#working-windows-quiet-hours). ### Editing workspace.toml while running The worker watches `workspace.toml` and reloads it automatically a moment after you save — no restart, and no missed tracker events or relay messages during the bounce: - **Routing rules, repos, defaults/team `task_query`, team `repo`, `[[automations]]`, `[[estimations]]`, `worker_task_args`, `poll_interval`, `worktrees_ttl_days`, and conflict-resolution mode/schedules apply to subsequent work.** Runs already in progress finish under the configuration they started with; everything picked up afterwards uses the new one. Changing a repo's `remote` updates its managed bare clone the next time that repo is prepared. - **Team identity and credentials are startup-only.** Restart after changing a team's name, tracker, `env_file`, or inline `[teams.env]` values. - **Error monitor clients are startup-only.** Restart after changing `[[error_monitors]]`, including project routing or source credentials. - **A broken edit never takes the daemon down.** The reload validates the file first; parse or schema errors are logged (naming the offending entries) and the last valid configuration keeps serving until you fix it. Rewriting identical content is ignored. - **Manual fallback:** send SIGHUP (`kill -HUP `) to force an immediate reload if file watching is unavailable on your system. - **Startup-only settings** still require a restart: tracker credentials in the workspace `.env` and `[defaults].tracker` (the tracker client and its detector are built once), `[worker.schedule]` quiet hours (the working-window gate is built once at startup), plus `[workspace].dashboard` / `dashboard_port`. A reload that changes one of these settings is rejected in full, so the active config remains internally consistent. `devintern worker init` can generate a user-level systemd unit on Linux or launchd agent on macOS. One service runs the whole workspace. For a hand-written Linux unit: ```ini [Unit] Description=DevIntern fleet worker After=network-online.target [Service] ExecStart=/usr/local/bin/devintern worker Restart=on-failure WorkingDirectory=/home/you/.devintern [Install] WantedBy=multi-user.target ``` ## Reviews, mentions, and the relay With GitHub credentials in the workspace `.env`, the fleet worker also reacts to PR activity across every GitHub repo in the workspace: - **The agent's own PRs**: one poller watches every PR the fleet created (the registry is shared across repos) and addresses actionable review feedback automatically. With `[workspace].ci_failure_fix = true`, it also repairs failing CI. Entries for repos no longer in `workspace.toml` are unwatched at startup. - **@mentions on any PR**: each GitHub repo gets a mention sweep. Mention-triggered runs are permission gated: the mentioning user needs write, maintain, or admin access, and the gate fails closed on API errors. Fork PRs are skipped unless maintainer edits are allowed. Standard workspaces recognize the central `devintern-ai` identity through the relay and use `GITHUB_TOKEN` for local API calls. No-relay installations need an advanced customer-owned App. - **Relay (instant events)**: accept relay setup in `devintern worker init`; its durable pairing is stored under the workspace home and starts automatically with the worker. GitHub envelopes carry the repository and route directly. Tracker events re-run the applicable defaults/team query and then use the same fixed mapping or routing rules as polling. A tracker type used by several teams stays polling-only because current relay envelopes do not identify the team registration; the worker fails closed instead of guessing. Events for repositories not in the workspace are ignored. To reconnect after adding repositories, run `devintern worker connect`. The command walks every GitHub repository in `workspace.toml`, skips already verified App pairings, and guides you through verification for the rest. `devintern worker connect status` also reports workspace repositories that still need verification. Tracker targets such as `devintern worker connect linear --team growth` compose the selected team's credentials on top of the shared workspace `.env`. Review and mention runs execute as subprocesses in the repo's persistent base checkout under `~/.devintern/worktrees//base`, with the same layered environment as task runs. ## Skipped tasks Ambiguous and unmatched tasks are recorded in the workspace database with the rules that matched. Fix the routing rules (or the task's labels), touch the task, and the worker picks it up on the next change. Skips never loop: a skipped task is not retried until it changes. --- # Create Tasks # @devintern/pm Configuration @devintern/pm uses per-project configuration stored in `.devintern-pm/.env` in your project directory. Run `devpm init` in a terminal for a guided setup: it asks which tracker you use, links to each provider's token creation page, validates the connection, and writes the file for you. Prefer editing by hand (or setting up in CI)? Run `devpm init --yes` to write the configuration template instead, then fill in values for your selected backend as described below. ## Select a Backend Set `TASK_TRACKER` to choose your PM tool. Defaults to `jira` if not specified. Supported backends: `jira`, `linear`, `trello`, `azure-devops`, `asana`, `github`, `gitlab`, `markdown` ```bash TASK_TRACKER=jira ``` ## Backend-Specific Configuration Only configure the section that matches your `TASK_TRACKER`. Other backend variables are ignored. ### Jira ```bash TASK_TRACKER=jira JIRA_BASE_URL=https://your-org.atlassian.net JIRA_EMAIL=your-email@example.com JIRA_API_TOKEN=your-api-token JIRA_DEFAULT_PROJECT_KEY=PROJ ``` Create an API token at [https://id.atlassian.com/manage-profile/security/api-tokens](https://id.atlassian.com/manage-profile/security/api-tokens). Use the Atlassian account email that owns the token for `JIRA_EMAIL`. **Project key**: the short prefix on issue keys (e.g. `PROJ` in `PROJ-123`). Find it in any issue URL or under **Project settings → Details → Key**. See the [Jira Integration guide](./jira-integration.md) for step-by-step setup and troubleshooting. ### Linear ```bash TASK_TRACKER=linear LINEAR_API_KEY=lin_api_xxxxxxxxxxxx # LINEAR_DEFAULT_TEAM_KEY=ENG # optional, first team if omitted ``` Create a **Personal API key** at [https://linear.app/settings/api](https://linear.app/settings/api) (Settings → API → Personal API keys). Keys start with `lin_api_` and cannot be viewed again after creation. **Team key**: the short prefix on issue identifiers (e.g. `ENG` in `ENG-42`). Find it under team Settings → Key, or pick a team in interactive mode (Ctrl+P). See the [Linear Integration guide](./linear-integration.md) for step-by-step setup and troubleshooting. ### Trello `TRELLO_API_TOKEN` is the only required variable: @devintern/pm includes a bundled Power-Up key so you don't need to register your own app. ```bash TASK_TRACKER=trello TRELLO_API_TOKEN=your-api-token # required # TRELLO_API_KEY=your-api-key # optional: use your own Power-Up # TRELLO_DEFAULT_BOARD_ID=abc123 # optional, first board if omitted # TRELLO_DEFAULT_LIST_NAME="To Do" # optional, first list if omitted ``` See the [Trello Integration guide](./trello-integration.md) for step-by-step setup. ### Azure DevOps ```bash TASK_TRACKER=azure-devops AZURE_DEVOPS_ORG=your-organization AZURE_DEVOPS_PAT=your-personal-access-token AZURE_DEVOPS_PROJECT=YourProject ``` All three variables are **required**. Use the organization slug from your URL (`https://dev.azure.com/your-org/...` → `your-org`), not the full URL. Create a **Personal Access Token** at `https://dev.azure.com/your-org/_usersSettings/tokens` with **Work Items (Read & write)** and **Project and Team (Read)** scopes. **Project name**: must match exactly as shown in Azure DevOps (from the URL path or project picker). Work item types depend on your process template (Agile, Scrum, Basic, etc.). See the [Azure DevOps Integration guide](./azure-devops-integration.md) for step-by-step setup and troubleshooting. ### Asana ```bash TASK_TRACKER=asana ASANA_API_TOKEN=your-asana-pat # ASANA_DEFAULT_PROJECT_GID=2222222222222222 # optional, first project if omitted ``` Create a token at [https://app.asana.com/0/developer-console](https://app.asana.com/0/developer-console). **Project GID**: the numeric ID after `/project/` in your project URL (e.g. `https://app.asana.com/1/…/project/2222222222222222/list/…` → `2222222222222222`). See the [Asana Integration guide](./asana-integration.md) for step-by-step setup and troubleshooting. ### GitHub Issues @devintern/pm creates issues in a repository via the GitHub REST API. ```bash TASK_TRACKER=github GITHUB_TOKEN=ghp_xxxxxxxxxxxx GITHUB_REPO=your-username-or-org/your-repo ``` **Personal Access Token**: both types work; fine-grained is recommended: - **Fine-grained:** [Generate token](https://github.com/settings/personal-access-tokens/new) with **Issues: Read and write** on the target repo - **Classic:** [Generate token](https://github.com/settings/tokens/new) with `repo` scope (private repos) or `public_repo` (public repos only) See the [GitHub Issues Integration guide](./github-integration.md) for step-by-step setup, label mapping, and troubleshooting. ### Markdown (local file export) ```bash TASK_TRACKER=markdown # MARKDOWN_TASKS_DIR=.devintern-pm/tasks # optional, defaults to .devintern-pm/tasks ``` Tasks are written as Markdown files under this directory (relative to the project root). ## Agent Harness Configure which AI agent CLI runs when generating stories and tasks: ```bash # Which harness to use (default: claude-code) AGENT_HARNESS=claude-code # Optional: path to the agent executable (leave unset in most cases) # AGENT_CLI_PATH=/custom/path/to/claude ``` In most cases you only need `AGENT_HARNESS`. By default each harness uses its standard command (for example `claude` for `claude-code`), and devintern locates it on your `PATH` automatically. Set `AGENT_CLI_PATH` only when the CLI is not on your `PATH` or uses a non-standard name. You can also override the harness for a single run with `--harness ` (CLI or interactive). In interactive mode, `Ctrl+G` opens a picker of installed agents; an explicit selection uses that harness's own path env vars and does not keep a previous `AGENT_CLI_PATH`. **Resolution order for the executable path:** 1. `AGENT_CLI_PATH` 2. Harness-specific env var (e.g. `OPENCODE_CLI_PATH` when `AGENT_HARNESS=opencode`) 3. Harness default command, located on your `PATH` (e.g. `claude`) Common `AGENT_HARNESS` values include `claude-code`, `opencode`, `codex`, `cursor`, `grok`, `deepseek`, `antigravity`, `cline`, `goose`, `kilo-code`, `kimi`, and `qwen`. If you do need to set a path explicitly, run `which` for the harness binary (`claude`, `opencode`, `codex`, `cursor-agent`, `grok`, `reasonix`, `agy`, `cline`, `goose`, `kilo`, `kimi`, or `qwen`). **Cursor note:** The Cursor harness uses Cursor's headless `cursor-agent` CLI (not a command named `cursor`). Cursor also installs an `agent` alias, but devpm looks for `cursor-agent` because other tools use the `agent` name too. Install Cursor and enable the CLI from Cursor's settings, then set `AGENT_HARNESS=cursor`. **Grok note:** Product name is Grok Build; the CLI binary is `grok`. Install from [x.ai/cli](https://x.ai/cli), authenticate (browser login or `XAI_API_KEY`), then set `AGENT_HARNESS=grok`. **DeepSeek note:** Harness id is `deepseek`; the CLI binary is `reasonix` (DeepSeek-Reasonix). Install with `npm i -g reasonix`, set `DEEPSEEK_API_KEY` (or run `reasonix setup`), then set `AGENT_HARNESS=deepseek`. **Antigravity note:** Harness id is `antigravity` (alias `agy`); the CLI binary is `agy`. Google retired consumer Gemini CLI on 2026-06-18 in favor of Antigravity CLI. Install from [antigravity.google/docs/cli/install](https://antigravity.google/docs/cli/install), authenticate (browser/keyring, or `ANTIGRAVITY_TOKEN` for CI), then set `AGENT_HARNESS=antigravity`. Legacy `AGENT_HARNESS=gemini` still routes to Antigravity with a deprecation warning. Prefer `AGENT_CLI_PATH` / `ANTIGRAVITY_CLI_PATH` / `AGY_CLI_PATH` over `GEMINI_CLI_PATH`. **Kilo Code note:** Harness id is `kilo-code`; the CLI binary is `kilo`. **Qwen note:** Qwen Code accepts a model via `--model` (e.g. `qwen3-coder-plus`); you can also keep the model in `~/.qwen/settings.json`. ### Model selection Set the model the agent harness runs with using `AGENT_MODEL` in `.devintern-pm/.env`: ```bash # .devintern-pm/.env AGENT_MODEL=sonnet ``` The model string is harness-specific — see your harness's CLI docs for accepted values (e.g. Claude Code aliases like `sonnet`, Codex/OpenAI model IDs, Antigravity slugs from `agy models`). For a single run, override it with `--model `; the flag wins over the environment. A few harnesses have no model flag and ignore the setting. In the DevIntern PM desktop app, set the same override per project from **Settings → Agent model**; it persists to `.devintern-pm/.env` (same file the CLI reads) and applies to new agent runs immediately. **Advanced spawn tuning** (rarely needed): ```bash # Retry attempts when the agent CLI path is momentarily missing (e.g. during an auto-update) # Default: 5 AGENT_SPAWN_ENOENT_RETRIES=5 # Initial backoff delay in milliseconds between retries (doubles each attempt) # Default: 1000 AGENT_SPAWN_ENOENT_BACKOFF_MS=1000 ``` These control how devpm handles a brief window where the agent CLI symlink is missing because the tool is updating itself. The defaults are sufficient for all common auto-updaters. ## Verbose API Logging Enable detailed API call logging for debugging: ```bash DEVINTERN_VERBOSE=1 ``` When set to `1` or `true`, every API request, response status, and retry attempt is printed to the console. This is useful for diagnosing authentication or connectivity issues. The same effect can be achieved at runtime by passing `--verbose` (or `-v`) to `devpm`. ## No License Required @devintern/pm is free to use under the FSL license: it performs no license check, trial gate, or `LICENSE_KEY` validation. Just run `devpm init` and start creating tasks. ## Environment File Location @devintern/pm searches for `.devintern-pm/.env` by traversing up from the current working directory to the project root (the nearest `.git` directory or your home directory). You can run `devpm` from any subdirectory of your project and it will find the correct config automatically. Run `devpm init` once per project to create this file (guided wizard in a terminal, or `devpm init --yes` for the template). ## Error Reporting The CLI and the DevIntern PM desktop app report errors to DevIntern's Sentry project by default so failures can be detected and fixed quickly. What is reported: - Crashes and unhandled errors in the CLI and the desktop app's main process - Failed desktop-app operations: agent runs (generate/edit/decompose stories, create tasks) and other IPC operations, with the failing channel as context - Renderer errors: uncaught window errors, unhandled promise rejections, and React crashes (reported with a component stack) What is **never** reported: prompts, ticket text, project paths, `.env` contents, tokens, or credentials. Error payloads are scrubbed of token-like strings before they are sent. To opt out: ```bash SENTRY_DISABLED=1 ``` Set this in your shell environment or in `.devintern-pm/.env`. In the desktop app, you can also disable **Settings → Anonymous analytics**: that toggle gates both usage analytics and error reporting (forwarded renderer errors included), and can be changed while the app is running. ## CLI Updates On startup, a globally installed `devpm` checks the npm registry (at most once per day) for a newer `@getdevintern/pm` version. | Mode | Behavior | | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | Interactive terminal | Offers an update prompt (`Update? (Y/n)`). Accepting installs the new version and re-runs your command. | | Non-interactive (CI, scripts, piped stdin) | **Skips install** (safe default). Prints a one-line notice at most once per check window. | | Opt-out | `DEVPM_NO_UPDATE=1` (or `DEVINTERN_NO_UPDATE=1`) or `--no-update` | | Opt-in auto-install (including non-interactive) | `DEVPM_AUTO_UPDATE=1` (or `DEVINTERN_AUTO_UPDATE=1`) | Only global npm or bun installs are updated. Monorepo checkouts, `bun link`, and local project `node_modules` installs are left alone. To upgrade immediately without waiting for the prompt or notice, reinstall globally with the package manager you installed with: ```bash npm install -g @getdevintern/pm@latest # or bun install -g @getdevintern/pm@latest ``` Update-check state (last check time, seen version) is cached per package in `~/.devintern/update-check.json`; delete that file to force a fresh registry lookup on the next run. ## Troubleshooting **"Missing required environment variables"** - Make sure you've run `devpm init` or copied `.env.example` to `.devintern-pm/.env` - Verify you've set the variables for your selected `TASK_TRACKER` (not every backend block) **"API error (401)"** - Verify your API token is correct for the selected backend - Check that your credentials match your account - For GitHub fine-grained tokens on org repos, confirm an admin has approved the token - For Linear, confirm `LINEAR_API_KEY` is the raw `lin_api_…` value (no `Bearer` prefix), see the [Linear Integration guide](./linear-integration.md#troubleshooting) - For Azure DevOps, confirm all three variables are set and the PAT has **Work Items (Read & write)**: see the [Azure DevOps Integration guide](./azure-devops-integration.md#troubleshooting) - For Jira, confirm `JIRA_EMAIL` matches the account that created the API token, see the [Jira Integration guide](./jira-integration.md#troubleshooting) **"GitHub API error (403)" or "(422)"** - See the [GitHub Issues Integration guide](./github-integration.md#troubleshooting) for token permissions, repository access, and label setup **"Unknown agent harness"** - Check `AGENT_HARNESS` spelling (use kebab-case, e.g. `claude-code`, `grok`, `deepseek`) - The error lists every valid harness name; pick one from that list - Ensure the matching CLI is installed and on your `PATH`, or set `AGENT_CLI_PATH` / `_CLI_PATH` **"Failed to parse story from agent output" / "malformed output"** - devpm automatically repairs common agent-output drift (markdown fences, narration, comments, trailing commas, unquoted keys, stray quotes) and re-runs the agent once with a strict-JSON reminder when repair isn't possible - If parsing still fails, the full agent output is saved to `/tmp/devpm--.log` — check the log to see what came back, then retry - Persistent failures usually mean the harness/model emits non-canonical JSON around long descriptions; switching harness (or pinning a different model) resolves it --- # Automation # @devintern/code Usage Guide ## Single Task Processing Process a single task from your configured tracker: ```bash # Jira (default) devintern TASK-123 # Linear (TASK_TRACKER=linear) devintern ENG-42 # Trello (TASK_TRACKER=trello) devintern 4uWKPOTv devintern https://trello.com/c/4uWKPOTv/card-slug --create-pr # Local markdown file (no PM credentials required) devintern ./tasks/feature-spec.md devintern /path/to/my-task.md --create-pr # Skip git branch creation devintern TASK-123 --no-git # Use custom .env file devintern TASK-123 --env-file /path/to/custom.env # Verbose output for debugging devintern TASK-123 -v # Custom AI agent CLI path devintern TASK-123 --agent-path /path/to/claude # Override max turns for very complex tasks (default: 500) devintern TASK-123 --max-turns 1000 # Skip automatic commit after AI agent completes devintern TASK-123 --no-auto-commit # Create pull request after implementation devintern TASK-123 --create-pr # Create PR targeting specific branch devintern TASK-123 --create-pr --pr-target-branch develop # Skip ALL task tracker comments (and Trello list transitions) devintern TASK-123 --skip-comments # Skip clarity check for faster processing devintern TASK-123 --skip-clarity-check # Re-run a task even if a previous attempt was reported incomplete # and the ticket is unchanged (normally you edit the description or # add a comment instead) devintern TASK-123 --force ``` ## Markdown File Tasks Pass one or more local `.md` files as arguments. No task tracker credentials are needed: ```bash # Single file devintern ./tasks/feature-spec.md --create-pr # Multiple files (processed in sequence) devintern ./epic.md ./subtask-a.md ./subtask-b.md # Mix a PM task with a local file devintern PROJ-123 ./extra-context.md ``` See the [Markdown File Tasks](./markdown-tasks.md) guide for frontmatter options, status tracking, and `TASK_TRACKER=markdown` mode. ## Batch Processing Process multiple tasks at once. The query syntax depends on the active tracker; `--query` accepts JQL for Jira, and either a JSON `IssueFilter` or a plain-text title search for Linear. See the per-tracker integration guides for full details. ### Jira ```bash # Process multiple specific tasks devintern PROJ-123 PROJ-124 PROJ-125 # Process tasks matching a JQL query (Jira only; --jql is a deprecated alias) devintern --query "project = PROJ AND status = 'To Do'" # Complex JQL with custom fields devintern --query "project = \"My Project\" AND cf[10016] <= 3 AND labels IN (FrontEnd, MobileApp)" # Batch process with PR creation devintern --query "assignee = currentUser() AND status = 'To Do'" --create-pr # High-complexity batch with extended turns devintern --query "labels = 'refactoring' AND type = Story" --max-turns 1000 --create-pr # Batch with skipped clarity checks devintern PROJ-101 PROJ-102 PROJ-103 --skip-clarity-check --create-pr ``` ### Linear ```bash # Process multiple specific issues devintern ENG-42 ENG-43 ENG-44 # Process issues with a given label (e.g. "intern") devintern --query '{"labels":{"name":{"eq":"intern"}}}' --create-pr # Process "In Progress" issues assigned to me devintern --query '{"state":{"name":{"eq":"In Progress"}},"assignee":{"isMe":{"eq":true}}}' --create-pr # High-priority issues devintern --query '{"priority":{"lte":2}}' --create-pr ``` Wrap JSON filters in single quotes so the shell does not interpret the inner double quotes. See the [Linear Integration guide](./linear-integration.md) for the full `IssueFilter` schema. ## Workflow Examples ### Standard Development Workflow ```bash # 1. Go to your project directory cd ~/projects/my-app # 2. Check git status (should be clean) git status # 3. Run devintern devintern MYAPP-456 # Expected output: # 🔍 Fetching JIRA task: MYAPP-456 # 📋 Task Summary: Implement user authentication # 💾 Saving formatted task details to: /tmp/devintern-tasks/myapp-456/task-details.md # 🌿 Creating feature branch... # ✅ Created and switched to new branch 'feature/myapp-456' # 🤖 Running Claude Code with task details... # [Agent implements the task...] # ✅ Agent execution completed successfully # 📝 Committing changes... # ✅ Successfully committed changes for MYAPP-456 ``` ## Git Integration Details ### Automatic Branch Creation - Creates branches with format: `feature/task-id` - Converts task keys to lowercase: `PROJ-123` → `feature/proj-123` - Checks for uncommitted changes before creating branches - Switches to existing branch if it already exists ### Automatic Commit - Commits all changes after AI agent successfully completes - Uses descriptive commit message: `feat: implement TASK-123 - Task Summary` - Can be disabled with `--no-auto-commit` flag - Skipped when the agent ends its run by asking you decision questions (for example "How should I proceed?" or a list of options). devintern prints the questions, posts them as a comment on the task, and stops without committing or opening a PR. Answer in the task and re-run. ### Pull Request Creation - Automatically creates PRs on GitHub or Bitbucket - Detects repository platform from git remote URL - PR title format: `[TASK-123] Task Summary` - PR body includes implementation details and links back to the task - Target branch can be specified with `--pr-target-branch`. If omitted (or if the named branch does not exist on the remote), the repository default branch is used - Target branch can also be auto-detected from the task description. Add a line like `Target branch: develop` to the card or issue and `devintern` will pick it up. Supported patterns: `Target branch:`, `Base branch:`, `PR target:`. Falls back to `--pr-target-branch` if no pattern matches. ## What It Does 1. **Fetches** task details (description, custom fields where supported, comments, attachments) 2. **Formats** the information for your AI agent 3. **Creates** a feature branch named `feature/task-id` 4. **Runs** optional feasibility assessment (skippable with `--skip-clarity-check`) 5. **Executes** your AI agent with enhanced permissions (default: 500 max turns) 6. **Saves** implementation summary to local files 7. **Commits** all changes automatically 8. **Pushes** the feature branch (when creating PRs) 9. **Creates** pull requests on GitHub or Bitbucket (optional) 10. **Posts** implementation results back to your task tracker (skippable with `--skip-comments`) ## Troubleshooting **"There are uncommitted changes"** - Commit your changes: `git add . && git commit -m "message"` - Or stash them: `git stash` - Or use `--no-git` to skip branch creation **"Agent reached maximum turns limit"** - Task is too complex for the current turn limit (default: 500) - Increase max turns: `--max-turns 1000` - Consider breaking the task into smaller subtasks **"PR creation failed"** - Ensure you have the correct token configured - Check token/App permissions - For GitHub App: Ensure the App is installed on the repository - Use `--verbose` flag to see detailed error messages **"Issue not found" / card fetch errors** - Check tracker credentials in `.devintern-code/.env` - Verify the task key or card ID exists and you have access - For Jira, ensure `JIRA_BASE_URL` is correct - For Trello, confirm `TASK_TRACKER=trello` and both `TRELLO_API_KEY` and `TRELLO_API_TOKEN` are set --- # Create Tasks # @devintern/pm Usage Guide [DevIntern PM](https://devintern.com/pm-desktop/) is the primary way to create and review tickets. This guide covers the CLI for terminal workflows and automation. ## Quick Capture (Desktop) DevIntern PM Desktop can register an OS-level global shortcut so you can turn fleeting context into ticket drafts without switching apps: - Enable it in **Settings → Quick Capture**. The default binding is `Cmd+Alt+Q` on macOS and `Ctrl+Alt+Q` on Windows/Linux, and you can record any combination. - Invoking the shortcut focuses (or launches) the app and opens a **new** ticket workspace as the active tab — open tickets and running agents keep working in the background. - If the clipboard holds useful text, it is prefilled and the source tab is inferred automatically (Figma URL → **Figma**, stack-trace-like text → **Error log**, anything else → **Prompt**). An empty clipboard opens an empty Prompt field with the cursor ready. - If no project is configured yet, capturing just brings the app forward so you can finish setup first. If the shortcut is already taken by another app, Settings shows how to change the binding. ## Interactive Mode (Recommended) The interactive mode provides a step-by-step terminal UI for creating tasks: ```bash devpm --interactive ``` Override the agent for a single session: ```bash devpm --interactive --harness opencode devpm --prompt "Add login" --harness codex ``` The interactive mode will guide you through: 1. **Source type selection**: Choose between Figma URL, error log, or free-form prompt 2. **Source input**: Enter your Figma URL, error log, or requirements 3. **Custom instructions** (optional): Add additional requirements or focus areas 4. **Epic linking** (optional): Link the story to an existing epic. This step is skipped for trackers that do not support a real epic/parent hierarchy (Trello, GitHub Issues, and Markdown). Supported by Jira, Linear, Azure DevOps, and Asana. 5. **Issue type** (Jira, Azure DevOps, GitHub, Markdown only): Select Story, Task, Bug, Epic, or enter a custom type. This step is skipped for Linear, Trello, and Asana, which do not support setting an issue type. 6. **Prompt style**: Choose between PM style or Technical style 7. **Confirmation**: Review your configuration before proceeding **Features:** - Step-by-step guided workflow - Keyboard navigation (Enter to confirm, ESC to go back, Ctrl+C to exit) - Header shows active tracker and project as `Tracker/Project` (e.g., `Jira/PROJ`, `Trello/My Board`) so you always know your context - Press `Ctrl+P` at any step to switch to a different project without restarting - Press `Ctrl+G` at any step to switch AI agent harness (only installed CLIs are listed) - Visual preview of your configuration - No need to remember command-line flags - Works great for both technical and non-technical users ## CLI Usage (For Power Users) For power users who prefer command-line flags: ```bash devpm --figma [options] devpm --log [options] devpm --prompt [options] ``` ### Source Options (one required) - `--figma `: Figma design node URL to analyze - `--log `: Error log or bug report text to analyze - `--prompt `: Free-form text describing requirements or features ### Additional Options - `--epic, -e `: Link the created story to an epic (e.g., PROJ-100). Ignored for trackers that do not support a real epic/parent hierarchy (Trello, GitHub Issues, GitLab, Markdown). - `--type, -t `: Issue type (default: "Task"). Common types: Task, Story, Bug, Epic. Only applied by backends that support issue types (Jira, Azure DevOps, GitHub, Markdown); ignored by Linear, Trello, and Asana. - `--custom, -c `: Additional custom instructions for the requirements - `--attach `: Attach a local file for agent context (and upload on create when the tracker supports it). Repeatable. Supported: images, text/docs, PDF (not Office binaries such as `.docx`). Max 10 files. - `--style, -s `: Prompt style: "pm" (default) or "technical" - **pm**: Focuses on user stories and acceptance criteria - **technical**: Includes Technical Considerations section - `--model, -m `: AI model to use (e.g., "sonnet", "opus", or full model name). Overrides the `AGENT_MODEL` environment variable. The model string is harness-specific (see your harness's CLI docs); unsupported by a few harnesses (e.g. Antigravity accepts slugs from `agy models`). - `--decompose`: Decompose the story into subtasks (default: off) - `--confirm`: Interactively confirm each subtask before creating - `--verbose, -v`: Enable verbose API logging for debugging (same as setting `DEVINTERN_VERBOSE=1`) - `--help, -h`: Show help message ### Examples **Figma designs:** > **Note**: Figma functionality requires the Figma MCP server to be installed and configured in your AI agent (Claude Code only). ```bash devpm --figma "https://www.figma.com/design/abc/file?node-id=123-456" devpm --figma "https://..." --epic PROJ-100 devpm --figma "https://..." -c "Focus on accessibility" devpm --figma "https://..." --style technical --decompose devpm --figma "https://..." --type Task ``` **Error logs:** ```bash devpm --log "Error: Cannot read property 'id' of undefined at line 42" devpm --log "$(cat error.log)" --epic PROJ-200 --type Bug devpm --log "Stack trace..." --style technical --model opus ``` **Free-form prompts:** ```bash devpm --prompt "Add user profile settings page with theme preferences" devpm --prompt "$(cat requirements.txt)" --epic PROJ-300 devpm --prompt "Implement OAuth login" --style technical --decompose devpm --prompt "Refine checkout" --attach ./notes.md --attach ./shot.png ``` ## Chat Bot Mode > **Alpha:** The chat bot is experimental and may not work properly. Expect bugs and breaking changes. Prefer creating tasks from Slack or Telegram? Run the bot daemon: ```bash devpm connect telegram # or: devpm connect slack devpm serve ``` Mention the bot with a rough idea, refine the draft in a thread, and approve it to file the task. See the [Chat bot guide](./chat-bot.md) for details. ### Attachments Attach local context files (screenshots, transcripts, roadmaps, specs) with `--attach` (CLI) or the Attach control in pm-desktop (Prompt and Error log tabs). - The agent reads attached files while drafting (Codex also receives images via native `-i` flags; other harnesses get file paths in the prompt). - After create, files are uploaded to the ticket when the tracker supports attachments: Jira, Linear, Azure DevOps, Asana, Trello, and Markdown. GitHub Issues has no file attachment API, so files stay draft-context only. - Prefer `.md`, `.txt`, `.pdf`, and images. Office formats such as `.docx` are rejected because coding agents cannot reliably read them. ## How It Works 1. **Input Analysis**: Your AI agent analyzes your input: - **Figma designs**: Uses the Figma MCP integration to extract design specifications - **Error logs**: Parses error messages and stack traces to identify root causes - **Free-form prompts**: Interprets requirements and feature descriptions - **Attachments** (optional): Local files listed for the agent to open before drafting 2. **Story Creation**: Creates a comprehensive story with: - User story format - Acceptance criteria - Technical considerations - Design notes (for Figma) or reproduction steps (for bugs) 3. **Epic Linking** (optional): Links the story to the specified epic. Only runs for trackers with a real epic/parent hierarchy (Jira, Linear, Azure DevOps, Asana). Skipped for Trello, GitHub Issues, and Markdown. 4. **Attachment upload** (optional): Uploads attached files to the created issue when the tracker supports it. 5. **Task Decomposition** (optional): Breaks down the story into subtasks that are: - Focused on single responsibilities - Completable within 1-2 days - Properly linked to the parent story --- # Automation # @devintern/code Configuration @devintern/code uses per-project configuration stored in `.devintern-code/.env` in your project directory. This allows you to work with multiple projects without configuration conflicts. The easiest way to create this file is `devintern init`. In a terminal it runs an interactive wizard: pick your tracker, follow the deep link to the provider's token page, paste the credentials, and the wizard verifies the connection before writing `.env`. Pass `--yes` (or `--no-interactive`) to skip the prompts and generate a commented template to fill in by hand, which is also what happens automatically when stdin is not a terminal (CI, scripts). ## Environment File Locations The tool searches for `.env` files in the following order: 1. **Custom path** (if specified with `--env-file`) 2. **Project discovery**: traverses up from the current working directory, checking `.devintern-code/.env` then `.env` at each level, stopping at the first `.git` root found or your home directory 3. **User home directory** (`~/.env`) 4. **Tool installation directory** You can run `devintern` from any subdirectory of your project and it will find the correct config automatically. ## Required Configuration The active task tracker is set with `TASK_TRACKER` (defaults to `jira`). Supported values: `jira`, `linear`, `trello`, `asana`, `azure-devops`, `github`, `gitlab`, `markdown`. ### Jira (default) Update your `.env` file with your Jira credentials: ```bash JIRA_BASE_URL=https://yourcompany.atlassian.net JIRA_EMAIL=your-email@company.com JIRA_API_TOKEN=your-api-token ``` Get your Jira API token at [https://id.atlassian.com/manage-profile/security/api-tokens](https://id.atlassian.com/manage-profile/security/api-tokens) See the [Jira Integration guide](./jira-integration.md) for setup, JQL batch runs, and troubleshooting. ### Linear Set `TASK_TRACKER=linear` and provide a Personal API key: ```bash TASK_TRACKER=linear LINEAR_API_KEY=lin_api_xxxxxxxxxxxx ``` Create a Personal API key at [https://linear.app/settings/api](https://linear.app/settings/api). Story points are written to Linear's built-in `estimate` field, so no custom field ID is required. See the [Linear Integration guide](./linear-integration.md) for state transitions and JSON `IssueFilter` batch runs. For unattended drains, use the [worker](./worker.md). ### Trello Set `TASK_TRACKER=trello` and provide Power-Up credentials: ```bash TASK_TRACKER=trello TRELLO_API_KEY=your-power-up-api-key TRELLO_API_TOKEN=your-user-token TRELLO_DEFAULT_BOARD_ID=abc123 # optional: board short ID from the board URL ``` See the [Trello Integration guide](./trello-integration.md) for token generation and list transition setup. ### Markdown (local files) To use local `.md` files as tasks without any PM credentials, set the tracker to `markdown` and point it at your tasks directory: ```bash TASK_TRACKER=markdown MARKDOWN_TASKS_DIR=/path/to/tasks ``` `MARKDOWN_TASKS_DIR` is required when `TASK_TRACKER=markdown`. `devintern` resolves bare task keys (e.g. `my-feature`) to `{MARKDOWN_TASKS_DIR}/my-feature.md`. You can also pass file paths directly as arguments without setting `TASK_TRACKER=markdown` at all. In that mode no `.devintern-code/.env` is needed for tracker credentials. See the [Markdown File Tasks guide](./markdown-tasks.md) for details. ## GitHub authentication ### Standard workspace setup Set `GITHUB_TOKEN` for interactive runs and workspace automation. When the workspace is paired with the hosted relay, the central [DevIntern AI App](https://github.com/apps/devintern-ai/installations/new) delivers events and the token stays on your machine for GitHub API reads/writes. You do not create a GitHub App or handle an App private key. Unattended runs also need a `LICENSE_KEY`. See [Pricing](https://devintern.com/pricing/). | What you want | Need | | ------------------------------------------------------------- | -------------------------------------------------------------- | | Implement tickets and open PRs from the CLI (personal) | `GITHUB_TOKEN` | | Use GitHub Issues as the task tracker (`TASK_TRACKER=github`) | `GITHUB_TOKEN` (the App cannot substitute) | | Worker review polling and replies | `GITHUB_TOKEN` | | `@devintern-ai` on any PR (standard workspace) | `GITHUB_TOKEN` + relay + central DevIntern AI App installation | | Air-gapped mentions or `devintern webhook serve` | Customer-owned GitHub App (advanced) | | Custom `slug[bot]` attribution | Customer-owned GitHub App (advanced) | See [GitHub Issues Integration](./github-issues-integration.md) and the advanced [GitHub Integration](./github-integration.md) guide. **Precedence when both are set:** - Relay-backed workspace: only `GITHUB_TOKEN` is used locally; custom App credentials are ignored - No-relay workspace: a complete customer-owned App is preferred, with `GITHUB_TOKEN` as fallback - CLI and PR creation: `GITHUB_TOKEN` is preferred - `devintern webhook serve`: the customer-owned App is preferred so its bot identity resolves Do not set `GITHUB_APP_ID` without `GITHUB_APP_PRIVATE_KEY_PATH` or `GITHUB_APP_PRIVATE_KEY_BASE64`. An ID alone is not a usable credential. ### Bot mention aliases Relay-backed workspaces recognize `devintern-ai` automatically. Advanced installations can add other bot logins as aliases: ```bash GITHUB_BOT_ALIASES=devintern-ai ``` The value is a comma-separated list of logins (with or without the `[bot]` suffix). Aliases count everywhere mentions are matched: commented reviews, inline comment scopes, and the `@mention` sweep. ### Which feedback gets re-processed Addressed feedback is tracked locally in the worker's state database, so a comment is never processed twice on the same machine. A 🎉 reaction is also left on each addressed comment as visual feedback for humans — it carries no gating meaning, so reaction-permission problems can never cause feedback to be re-processed. ### GitHub Personal Access Token For interactive CLI use, relay-backed workspaces, and `TASK_TRACKER=github`: ```bash GITHUB_TOKEN=your-github-token ``` - **Classic token**: Requires `repo` scope - **Fine-grained token** (recommended): Requires `Pull requests: Read and write` and `Contents: Read and write` permissions. When `[workspace].ci_failure_fix = true`, also grant `Actions: Read` and `Commit statuses: Read`. Add `Issues: Read and write` when `TASK_TRACKER=github` - Create at: [https://github.com/settings/tokens](https://github.com/settings/tokens) > **`Contents` must be _Read and write_, not Read.** Branch pushes go through the same credential as everything else, and a Contents-readonly token passes every API check (task fetch, PR reads) while `git push` fails with `403 ... denied to `. If your setup delegates pushing to an SSH remote instead (`git@github.com:owner/repo.git`), the PAT does not need `Contents: Write` for pushes. #### How git picks push credentials Pushes use git's ambient credential chain — devintern does not inject tokens into `git push`: 1. If `gh auth git-credential` is configured (typical with the GitHub CLI) and `$GITHUB_TOKEN` is exported in the environment, **the environment variable wins over your keyring login**. An under-scoped `GITHUB_TOKEN` therefore silently overrides a working `gho_…` login. 2. Otherwise the keyring/token-helper credentials apply. 3. SSH remotes use your SSH keys. The worker dry-runs a push against each configured GitHub HTTPS remote at startup and warns when it is rejected (`✅ [fleet] push access verified for ` / a `⚠️ [fleet] … rejects pushes` line), so credential problems surface before the first task burns its pickup. ### Advanced: customer-owned GitHub App Use this only when the hosted relay cannot be used—for example an air-gapped installation—or when operating `devintern webhook serve` against your own endpoint. It provides no-relay `@mention` identity, installation API tokens, and custom `slug[bot]` attribution. ```bash GITHUB_APP_ID=123456 GITHUB_APP_PRIVATE_KEY_PATH=/secure/path/to/your-app.private-key.pem ``` Both the ID and a private key are required. **Benefits:** - Can create PRs without a personal token (team/automation setups) - Fine-grained permissions - Centralized control - Audit trail - Resolves the bot identity required for `@mention` matching **Setup steps:** 1. Go to **Settings → Developer settings → GitHub Apps → New GitHub App** 2. Set repository permissions: - **Contents:** Read and write - **Pull requests:** Read and write - **Issues:** Read and write - **Actions:** Read (when automatic CI failure fixes are enabled) - **Commit statuses:** Read (when automatic CI failure fixes are enabled) 3. Generate and save a private key 4. Install the App on your repositories > These permissions cover task implementation, PR creation, and the 🎉 reaction left on addressed feedback. The reaction is cosmetic only — whether feedback needs action is decided from the local state database — so a missing reaction permission never causes re-processing. If reactions fail with a permissions error after a settings change, re-approve the installation; already-issued credentials keep working for up to an hour. If you also run the webhook server or mention sweep to auto-address PR feedback, that App needs additional **Pull request review comments** and **Issue comments** permissions plus event subscriptions; see [GitHub Integration](./github-integration.md#update-app-permissions). For CI/CD environments, you can use a base64-encoded key: ```bash GITHUB_APP_ID=123456 GITHUB_APP_PRIVATE_KEY_BASE64=LS0tLS1CRUdJTi4uLg== ``` To encode your key: ```bash # macOS base64 -i your-app.private-key.pem # Linux base64 -w 0 your-app.private-key.pem ``` ### Bitbucket ```bash BITBUCKET_TOKEN=your-bitbucket-token ``` Requires `Repositories: Write` permission. Create at [https://bitbucket.org/account/settings/app-passwords/](https://bitbucket.org/account/settings/app-passwords/) ## Per-Project Settings The `.devintern-code/settings.json` file allows project-specific behavior. Settings are organized by task tracker, so you can configure multiple trackers in one file. ```json { "jira": { "projects": { "PROJ": { "prStatus": "In Review", "inProgressStatus": "In Progress", "todoStatus": "To Do", "storyPointsField": "customfield_10016" } } }, "linear": { "projects": { "ENG": { "prStatus": "In Review", "inProgressStatus": "In Progress", "todoStatus": "Backlog" } } }, "trello": { "projects": { "abc123": { "prStatus": "Review", "inProgressStatus": "Doing", "todoStatus": "To Do" } } } } ``` The active tracker is read from the `TASK_TRACKER` environment variable (defaults to `jira`). Run `devintern init` to generate a `settings.json` with examples for all supported trackers. **Fields (all optional):** - `prStatus`: Status/state/label to transition to after PR creation (e.g., "In Review") - `inProgressStatus`: Status to set when starting work (e.g., "In Progress", "Doing") - `todoStatus`: Status to reset to if implementation fails (e.g., "To Do", "Backlog") - `storyPointsField`: Custom field ID for story points (e.g., `"customfield_10016"` for Jira); auto-discovered if omitted **Supported trackers:** `jira`, `linear`, `trello`, `asana`, `azure-devops`, `github`, `gitlab`, `markdown`. **Backward compatibility:** Existing Jira-only files using the legacy top-level `projects` key continue to work without any changes. ```json { "projects": { "PROJ": { "prStatus": "In Review" } } } ``` ## Verbose API Logging To enable detailed API call logging for debugging, set the `DEVINTERN_VERBOSE` environment variable: ```bash DEVINTERN_VERBOSE=1 ``` This logs every API call, response, and retry attempt to the console. Leave it unset (the default) for quiet operation. ## Error Reporting The CLI reports errors to DevIntern's Sentry project by default so failures can be detected and fixed quickly. What is reported: - Crashes and unhandled errors (any entry point: task runs, `worker`, `webhook serve`, `dashboard`, subcommands) - Task runs that fail mid-pipeline (agent errors, tracker/PR API failures) with context such as the task key, active tracker, and pipeline stage - Failed PR creation, failed estimation, and failed commits (which continue without completing that step) - Webhook review/comment processing failures What is **never** reported: task content, code, `.env` contents, tokens, or credentials. Error payloads are scrubbed of token-like strings before they are sent, and contexts contain only identifiers (task key, tracker type, stage/command names). To opt out: ```bash SENTRY_DISABLED=1 ``` Set this in your shell environment or in `.devintern-code/.env`. ## Anonymous Usage Analytics The CLI sends one anonymous usage event per run to DevIntern's PostHog project so we can understand popularity and which features are used. It never sends task content, code, repository names, file paths, or credentials — only: - CLI version, OS, architecture - Active tracker type (e.g. `jira`, `linear`) and run mode (tasks / query / estimate) - Task count and boolean feature flags (`--create-pr`, `--auto-review`, `--estimate`, sandbox provider) - Whether the session runs in CI A random anonymous ID is generated once per project and stored in `.devintern-code/telemetry.json`. Analytics are disabled automatically when running from source. To opt out, either: ```bash # Shell or .devintern-code/.env DEVINTERN_TELEMETRY_DISABLED=1 ``` or set in `.devintern-code/settings.json`: ```json { "analytics": { "enabled": false } } ``` See [devintern.com/privacy](https://devintern.com/privacy/) for details. ## Readiness Check Run `devintern doctor` for a one-screen answer to "is everything set up?": ```bash devintern doctor ``` It checks, in order: - **Bun runtime** and **Git** availability - **AI agent CLI**: whether your configured harness (`AGENT_HARNESS`, default `claude-code`) is installed and on `PATH`; suggests an installed alternative or install steps when not - **Task tracker credentials**: required environment variables for your `TASK_TRACKER` in `.devintern-code/.env` - **DevIntern sign-in**: local session validity (`devintern login` when missing) - **License**: entitlement status when signed in (only needed for unattended automation) Each failing row gets a fix hint. The command exits non-zero when any check fails, so scripts and CI can gate on it. The interactive `devintern init` wizard runs a subset of these checks automatically at the end of setup. ## Output Directory By default, task artifacts are saved to `/tmp/devintern-tasks`. You can customize this: ```bash DEVINTERN_OUTPUT_DIR=./devintern-output ``` Everything in the output directory is a debug artifact and safe to delete. Retry bookkeeping (which tasks were reported incomplete, and when) lives in `.devintern-code/queue.db` in your project instead, so it survives reboots. **Output structure:** ``` {output-dir}/{task-key}/ ├── task-details.md # Formatted task for AI agent ├── feasibility-assessment.md # Clarity check results ├── implementation-summary.md # Success output ├── implementation-summary-incomplete.md # Failure output ├── auto-review-summary.json # Auto-review loop results ├── iteration-{N}/ # Auto-review iterations │ ├── feedback.json │ └── review-prompt.txt └── attachments/ # Jira attachments ``` ## Agent Harness Configure which AI agent runs and how long it can work: ```bash # Agent harness type (default: claude-code) AGENT_HARNESS=claude-code # Optional: path to the agent CLI (leave unset in most cases) # AGENT_CLI_PATH=/custom/path/to/claude # Optional: model the harness runs with (harness-specific string) # AGENT_MODEL=sonnet ``` You usually only need `AGENT_HARNESS`. By default devintern uses the harness's standard command (for example `claude` for `claude-code`) and finds it on your `PATH` automatically, so `AGENT_CLI_PATH` can be left unset. Common `AGENT_HARNESS` values include `claude-code`, `opencode`, `codex`, `cursor`, `grok`, `deepseek`, `antigravity`, `cline`, `goose`, `kilo-code`, `kimi`, and `qwen`. If you do need to set a path explicitly, run `which` for the harness binary (`claude`, `opencode`, `codex`, `cursor-agent`, `grok`, `reasonix`, `agy`, `cline`, `goose`, `kilo`, `kimi`, or `qwen`). **Cursor note:** The Cursor harness uses Cursor's headless `cursor-agent` CLI (not a command named `cursor`). Cursor also installs an `agent` alias, but devintern looks for `cursor-agent` because other tools use the `agent` name too. Install Cursor and enable the CLI from Cursor's settings, then set `AGENT_HARNESS=cursor`. The `--max-turns` option has no effect with this harness; Cursor runs until the task is complete. **Grok note:** Product name is Grok Build; the CLI binary is `grok`. Install from [x.ai/cli](https://x.ai/cli), authenticate (browser login or `XAI_API_KEY`), then set `AGENT_HARNESS=grok`. `--max-turns` has no effect; Grok runs until the task completes. **DeepSeek note:** Harness id is `deepseek`; the CLI binary is `reasonix` (DeepSeek-Reasonix, listed in DeepSeek's agent integrations). Install with `npm i -g reasonix`, set `DEEPSEEK_API_KEY` (or run `reasonix setup`), then set `AGENT_HARNESS=deepseek`. `--max-turns` and permission-skip flags have no effect on `reasonix run` (turn limits live in Reasonix config; headless runs are already autonomous). **Antigravity note:** Harness id is `antigravity` (alias `agy`); the CLI binary is `agy`. Google retired consumer Gemini CLI on 2026-06-18 in favor of Antigravity CLI. Install from [antigravity.google/docs/cli/install](https://antigravity.google/docs/cli/install), authenticate (browser/keyring, or `ANTIGRAVITY_TOKEN` for CI), then set `AGENT_HARNESS=antigravity`. Legacy `AGENT_HARNESS=gemini` still routes to Antigravity with a deprecation warning; DevIntern does not spawn the retired `gemini` binary. Prefer `AGENT_CLI_PATH` / `ANTIGRAVITY_CLI_PATH` / `AGY_CLI_PATH` over `GEMINI_CLI_PATH`. `--max-turns` has no effect; model selection accepts slugs from `agy models` via `AGENT_MODEL`. **Kilo Code note:** Harness id is `kilo-code`; the CLI binary is `kilo`. **Qwen note:** Qwen Code accepts a model via its `--model` flag (e.g. `qwen3-coder-plus`) — set it with `AGENT_MODEL`; you can also keep the model in `~/.qwen/settings.json`. ### Failover across multiple harnesses `AGENT_HARNESS` accepts a comma-separated, priority-ordered list so the unattended worker keeps processing when an agent hits its usage limit: ```bash # .devintern-code/.env AGENT_HARNESS=claude-code,codex ``` The first entry is your preferred harness; later entries are fallbacks in priority order. A single value behaves exactly as before. **Failover behavior (worker mode):** Applies to every unattended worker surface — fleet task polling, PR review addressing, `@mention` runs, conflict resolution, scheduled automations, estimations, dashboard retries, relay-driven tasks, and `devintern webhook serve` — not only the webhook queue. - At startup every entry is checked against the harness registry and your machine: unknown or not-installed entries produce a clear warning and are skipped, and the effective chain is logged (e.g. `Agent harness: claude-code → codex (failover enabled)`). - When the active harness reports a usage/rate limit, the worker records its reset window (parsed from the limit output; a 1-hour cooldown applies when no timer is parseable, e.g. monthly spend limits) and immediately retries the same work on the highest-priority harness that still has capacity. - When the primary harness's window elapses, the worker automatically fails back to it and logs the switch. Fallback agents hitting their own limits mid-run advance the chain again. - If every harness in the chain is limited at once, new agent work is deferred until the earliest window ends (the webhook queue pauses; polling/review/automation runs return to their next tick). - Failover state (active harness + per-harness windows) persists in the queue database, so restarting the worker resumes on the right harness instead of retrying a still-limited agent. - Which harness executed each run is recorded in run records, and `/health` on the webhook server reports the active harness, the chain, and open limit windows. Interactive one-shot runs you start yourself (`devintern TASK-123` in a terminal) always use the first (priority) entry; the worker pins each subprocess to the active harness so failover can switch the next attempt. **Per-harness overrides inside a list:** `_CLI_PATH` (e.g. `CODEX_CLI_PATH`) resolves per active harness at spawn time. The global `AGENT_CLI_PATH` applies to the first entry only, so a stale global override cannot leak onto a fallback agent. `AGENT_MODEL` applies to whichever harness is active (the string is harness-specific). ### Model selection Set the model the agent harness runs with using `AGENT_MODEL` in `.devintern-code/.env`: ```bash # .devintern-code/.env AGENT_MODEL=sonnet ``` The model string is harness-specific — see your harness's CLI docs for accepted values (e.g. Claude Code aliases like `sonnet`, Codex/OpenAI model IDs, Antigravity slugs from `agy models`). DevIntern passes it to every agent spawn (implementation runs, analysis, reviews, and hook fixes). A few harnesses have no model flag and ignore the setting. Set `AGENT_CLI_PATH` only when the CLI is not on your `PATH` or uses a non-standard name. You can give it a bare command name or a full path. Avoid committing an **absolute** path to a shared `.env`: it is machine-specific, so copying an `.env` from macOS (`/Users/...`) to a Linux host (`/home/...`) would point at a non-existent binary. If the configured command cannot be found, devintern fails fast at startup with a message telling you the CLI is not on your `PATH`. **Advanced spawn tuning** (rarely needed): ```bash # Retry attempts when the agent CLI path is momentarily missing (e.g. during an auto-update) # Default: 5 AGENT_SPAWN_ENOENT_RETRIES=5 # Initial backoff delay in milliseconds between retries (doubles each attempt) # Default: 1000 AGENT_SPAWN_ENOENT_BACKOFF_MS=1000 ``` These control how devintern handles a brief window where the agent CLI symlink is missing because the tool is updating itself. The defaults are sufficient for all common auto-updaters. **CLI options:** - `--agent-path`: override the agent executable path - `--max-turns`: max turns for **implementation** (default: **500**; clarity checks always use 10) - `--skip-clarity-check`: skip feasibility assessment before implementation DevIntern always runs the full workflow after fetching a task (clarity check → agent → commit/PR). There is no fetch-only mode. ## Sandboxing the Agent Agents run with their own permission prompts disabled (the equivalent of `--dangerously-skip-permissions`), so by default they have the same access to your machine as your user account. DevIntern can wrap every agent run in an OS-level sandbox so the agent stays confined even in fully automated worker runs. ```bash # In the workspace .env (~/.devintern/.env) AGENT_SANDBOX=auto ``` Or per interactive run: ```bash devintern PROJ-123 --sandbox nono ``` Run `devintern sandbox` at any time for a full diagnosis: which providers are installed, the one-time setup steps each still needs, which one `auto` would pick, and exactly what your next run will do with the current configuration, including why it would fail. The command exits non-zero when the configured provider guarantees a failed run, so scripts and CI can gate on it. ### Providers | Value | What it is | Works with | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | `none` | No sandboxing (default) | all harnesses | | `auto` | Best available option (native, then nono, then srt); runs unsandboxed with a warning if none is available. Providers needing per-user setup (docker, smolvm) are never picked automatically | depends on pick | | `native` | The harness's own built-in OS sandbox, enabled and configured by DevIntern. Zero install, nothing to set up | `claude-code`, `codex` | | `nono` | Kernel-enforced isolation (Landlock on Linux, Seatbelt on macOS), zero setup ([nono.sh](https://nono.sh)) | all harnesses | | `srt` | [Anthropic Sandbox Runtime](https://github.com/anthropic-experimental/sandbox-runtime): sandbox-exec on macOS, bubblewrap on Linux (`npm install -g @anthropic-ai/sandbox-runtime`) | all harnesses | | `docker` | [Docker Sandboxes](https://www.docker.com/products/docker-sandboxes) microVM via the `sbx` CLI; never picked by `auto` (requires per-user setup, see below) | `claude-code`, `codex`, `cursor`, `gemini`, `opencode` | | `smolvm` | [SmolVM](https://docs.celesto.ai/smolvm/introduction) microVM with a dedicated in-VM browser sandbox; never picked by `auto` | `claude-code`, `codex`, `pi` | If you explicitly select a provider that is not installed or does not support your harness, the run fails with an actionable error rather than silently running unsandboxed. ### What the sandbox allows The default policy confines filesystem writes to what a task run actually needs: your project working directory, the task output directory, the system temp directory, and the Playwright and Puppeteer browser caches (so agents that launch a browser for testing or research keep working). Network access stays open, since agents need to reach model APIs, push to git remotes, and install packages. Optional restrictions: ```bash # Extra writable paths (colon-separated) AGENT_SANDBOX_WRITABLE_PATHS=/data/scratch:/var/cache/myapp # Restrict network egress to specific domains (strictly enforced by srt; # nono and docker apply it via their own network filters; smolvm ignores it) AGENT_SANDBOX_ALLOWED_DOMAINS=api.anthropic.com,github.com # nono: use a custom profile instead of nono's default scope AGENT_SANDBOX_NONO_PROFILE=my-profile ``` ### Provider setup at a glance Every provider needs its tool installed; some also need auth or one-time configuration before the first sandboxed run. `devintern sandbox` reports missing pieces per provider. | Provider | Install | Agent auth inside the sandbox | One-time setup | Picked by `auto` | Docs | | -------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------- | | `native` | none (built into the harness) | Your existing agent login | none | yes (first pick) | [native sandbox details](#claude-code-native-sandbox-details) | | `nono` | macOS: `brew install nono`; Linux: `curl -fsSL https://nono.sh/install.sh \| sh` | Your existing agent login (the agent pack grants macOS Keychain access) | Per agent: `nono pull nolabs-ai/` (packs exist for claude, codex, opencode, goose, pi, antigravity; Sigstore-verified) | yes | [nono.sh/docs](https://nono.sh/docs) | | `srt` | `npm install -g @anthropic-ai/sandbox-runtime` (+ `bubblewrap`, `socat`, `ripgrep` on Linux) | Your existing agent login | none, but the network runs on an explicit allowlist (see below) | yes | [sandbox-runtime](https://github.com/anthropic-experimental/sandbox-runtime) | | `docker` | macOS: `brew install docker/tap/sbx`; Windows: `winget install Docker.sbx`; Linux: `docker-sbx` (Docker's apt repo, or the AUR) | API key via `sbx secret set -g anthropic` (also `google`, `github`, `openai`, ...); host logins are not visible inside the VM | `sbx login` (Docker Desktop; not needed on native Linux), then `sbx policy init balanced` | no | [docs.docker.com/ai/sandboxes](https://docs.docker.com/ai/sandboxes/) | | `smolvm` | `curl -sSL https://celesto.ai/install.sh \| bash` | Host agent credentials are forwarded automatically | first run downloads a guest VM image (several minutes); Linux also needs KVM and your distro's QEMU system package | no | [docs.celesto.ai/smolvm](https://docs.celesto.ai/smolvm/introduction) | ### Provider notes - **native** uses the sandbox already built into the harness: Claude Code's sandboxed Bash tool (see [Claude Code native sandbox details](#claude-code-native-sandbox-details) for the exact generated configuration) and Codex's `--sandbox workspace-write`. It needs no installation, and the agent understands its own sandbox errors, so it retries intelligently instead of failing on mysterious permission errors. Scope caveat: for Claude Code the native sandbox confines shell commands and their child processes, not the agent's built-in file tools. For a boundary around the whole agent process, use `nono` or `srt`. Antigravity (the Gemini CLI successor) is not supported in native mode: its built-in sandbox covers shell commands only and is bypassed during unattended runs, so use `nono` or `srt` with it. - **nono and srt** wrap the agent process directly, so any harness works and worktree-based flows (webhook reviews, `address-review`) are fully supported. On Linux, srt also requires `bubblewrap`, `socat`, and `ripgrep`. DevIntern translates the sandbox policy into each tool's own configuration, including read access to your home directory and write access to the agent's own state files (without which agents start logged out). When one of these wraps Codex on macOS, DevIntern automatically disables Codex's own sandbox for that run (nested macOS sandboxes are unsupported; the outer wrapper is the enforcement boundary). On Linux both layers stack. - **nono** works best with the official agent pack for your harness installed: run `nono pull nolabs-ai/` once (packs exist for `claude`, `codex`, `opencode`, `goose`, `pi`, and `antigravity`; each is Sigstore-verified on pull), and DevIntern automatically uses the pack profile for matching runs. The packs carry grants plain flags cannot express, such as macOS Keychain access; without the claude pack, `claude-code` under nono reports "Not logged in" on macOS. Browse packs at [registry.nono.sh](https://registry.nono.sh). On Linux, DevIntern generates a composite profile combining the pack (when installed) with nono's built-in `linux-host-compat` profile plus `/etc` read access, and automatically resolves Landlock conflicts between broad read grants and nono's protected paths, so runs work out of the box with no profile setup. DevIntern also grants `/dev/ptmx` so tools that allocate a PTY (for example lefthook during `git commit`) work inside the sandbox. - **srt cannot run with a fully open network.** Its settings schema requires an explicit domain allowlist and rejects `*`, so with no `AGENT_SANDBOX_ALLOWED_DOMAINS` set, DevIntern applies a built-in allowlist of agent essentials: model APIs (Anthropic, OpenAI, Google), git hosts (GitHub, GitLab, Bitbucket), and the common package registries (npm, PyPI, crates.io, Go, Maven). A task that needs another host will see its requests denied with a 403; add the domain via `AGENT_SANDBOX_ALLOWED_DOMAINS`. Localhost is unaffected (local binding is always allowed for dev servers and browser automation). - **Docker Sandboxes** run the agent inside a microVM that mounts your project directory as the workspace. It now ships as the standalone `sbx` CLI (the old `docker sandbox` plugin was removed). One-time setup: install `sbx` (see the table above), sign in with `sbx login` on Docker Desktop (native Linux with the local daemon needs no sign-in), run `sbx policy init balanced`, and store an API key for the in-VM agent with `sbx secret set anthropic` (the guest agent cannot use your host login). Host paths under `/tmp` are not part of the mounted workspace, so review worktree flows are not supported with this provider; use `nono` or `srt` for those, or move `DEVINTERN_OUTPUT_DIR` out of `/tmp`. Network follows the sandbox policy: DevIntern adds per-sandbox rules on top of your global `sbx` policy, allowing all hosts by default (the open-network policy) or exactly your `AGENT_SANDBOX_ALLOWED_DOMAINS` list when set. The rules are scoped to each run's sandbox and removed with it, so your global `sbx policy init` choice still governs everything you run outside DevIntern. - **SmolVM** is the strongest option for browser-heavy workloads (it can run a full browser inside the VM), and it forwards your host agent credentials into the sandbox automatically. Each run boots a fresh microVM with your working directory mounted read-write at its host path, runs the agent inside it, and deletes the VM afterward; the first run downloads a guest image, which takes several minutes. It is a young project, so you must opt in explicitly with `AGENT_SANDBOX=smolvm`. On Linux, DevIntern runs SmolVM with its QEMU backend (workspace mounts need it), which requires KVM (`/dev/kvm`) and your distro's QEMU system package (for example `qemu-system-x86` plus `qemu-img`); `devintern sandbox` reports both when missing. Note the name collision: this provider targets [CelestoAI SmolVM](https://docs.celesto.ai/smolvm/introduction) (installed via `curl -sSL https://celesto.ai/install.sh | bash`), not the unrelated smol-machines microVM runner that installs a binary with the same name; `devintern sandbox` detects and reports the wrong one. - **Git over ssh** behaves differently per wrapper and OS. Under `nono` on Linux, ssh pushes work out of the box: the reads-open policy leaves `~/.ssh` readable (Landlock cannot re-deny inside an explicit grant, so this matches the srt behavior where key files also stay readable) and the ssh-agent socket is forwarded when present. Under `nono` on macOS, Seatbelt keeps private keys in `~/.ssh` blocked; load your key into the ssh-agent instead (`ssh-add ~/.ssh/id_rsa`, the agent socket is forwarded into the sandbox) and grant read access to `~/.ssh/known_hosts` with a small user profile, saved to `~/.config/nono/profiles/` and selected via `AGENT_SANDBOX_NONO_PROFILE=claude-code-ssh`: ```json { "extends": "claude-code", "meta": { "name": "claude-code-ssh", "description": "claude-code pack + git push over ssh (keys stay in the ssh-agent)" }, "filesystem": { "read_file": ["$HOME/.ssh/known_hosts"], "bypass_protection": ["$HOME/.ssh/known_hosts"] } } ``` Under `srt`, all traffic goes through its HTTP proxy and raw ssh connections cannot resolve or reach hosts, so use HTTPS git remotes there. - **Browser automation inside the sandbox** depends on the wrapper and OS. Under `nono` on Linux, standard Playwright launches work unchanged (the composite profile grants the system reads Chromium needs). Under `srt` on Linux, launch Chromium with `chromium.launch({ args: ["--disable-gpu"] })`; Chromium's own sandbox can stay on, and note that the macOS single-process recipe does not work on Linux. Under `srt` on macOS, Playwright works when Chromium is launched with its own sandbox off and in single-process mode (`chromium.launch({ chromiumSandbox: false, args: ["--single-process", "--no-sandbox"] })`); the outer sandbox still confines the whole process. Under `nono` on macOS, Chromium needs a user profile that opts into a few Seatbelt capabilities the default policy blocks (tracked upstream in [nono#1019](https://github.com/nolabs-ai/nono/issues/1019)); with the profile below saved to `~/.config/nono/profiles/` and selected via `AGENT_SANDBOX_NONO_PROFILE=claude-code-browser`, standard Playwright launches work unchanged: ```json { "extends": "claude-code", "meta": { "name": "claude-code-browser", "description": "claude-code pack + Chromium launch support" }, "unsafe_macos_seatbelt_rules": [ "(allow iokit-open (iokit-user-client-class \"RootDomainUserClient\") (iokit-user-client-class \"IOSurfaceRootUserClient\"))", "(allow iokit-get-properties)", "(allow mach-register)" ] } ``` SmolVM's in-VM browser is the alternative when you'd rather not extend the wrapper policy. - Changing `AGENT_SANDBOX` in `.env` applies to the next one-shot run immediately. A running `devintern worker` daemon reads `.env` at startup, so restart it after changing the value (same as `AGENT_HARNESS`). ### Claude Code native sandbox details With `AGENT_SANDBOX=native` and `AGENT_HARNESS=claude-code`, DevIntern generates a settings file for each run and passes it to the agent with `--settings`. This is what it contains and why: | Setting | Value | Why | | ----------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sandbox.enabled` | `true` | Turns the Bash-tool sandbox on | | `sandbox.autoAllowBashIfSandboxed` | `true` | Sandboxed commands run without prompts; the sandbox boundary replaces the permission prompt | | `sandbox.allowUnsandboxedCommands` | `false` | Closes the escape hatch. By default Claude Code may retry a sandbox-blocked command outside the sandbox; combined with skip-permissions that retry would be silently approved, making the sandbox advisory instead of a boundary | | `sandbox.failIfUnavailable` | `true` | If the sandbox cannot start (for example missing `bubblewrap` on Linux), the run fails with a clear error instead of silently running unsandboxed. You explicitly asked for isolation, so DevIntern never quietly drops it | | `sandbox.filesystem.allowWrite` | working directory, task output directory, temp directory, browser caches | The write set a DevIntern task actually needs | | `sandbox.network.allowedDomains` | `["*"]` (or your `AGENT_SANDBOX_ALLOWED_DOMAINS` list) | The sandbox default is deny-with-prompt for every new domain. In non-interactive runs nobody can approve the prompt, so with the escape hatch closed all network commands would fail. Open must be stated explicitly | | `sandbox.network.allowLocalBinding` | `true` | Dev servers and browser automation bind to localhost; the sandbox blocks that by default | | `sandbox.network.allowUnixSockets` | your `SSH_AUTH_SOCK`, when set | Lets `git push` over SSH reach the ssh-agent | | `sandbox.excludedCommands` | `["docker *", "gh *"]` | See below | **Why `docker` and `gh` are excluded.** Claude Code's own documentation lists `docker` as incompatible with the sandbox, and Go-based CLIs such as `gh` fail TLS verification under macOS sandboxing. Because DevIntern closes the unsandboxed-retry escape hatch, these commands would hard-fail inside the sandbox and there would be no fallback. Excluding them means they run outside the sandbox, which is exactly how they run today without any sandbox: for these two commands the native provider is no worse than the status quo, and everything else stays confined. If you do not want these exceptions, use `nono` or `srt` instead, which wrap the whole process without per-command carve-outs. **Extending the generated configuration.** Claude Code merges settings across scopes: array settings such as `allowWrite`, `denyRead`, and `excludedCommands` combine entries from every scope, so your own `.claude/settings.json` (project) or `~/.claude/settings.json` (user) can narrow or widen the policy DevIntern generates. Settings worth considering that DevIntern does not set for you: - `sandbox.credentials`: the sandbox's default read policy still allows reading files like `~/.ssh` and `~/.aws/credentials`. Credential entries with `"mode": "deny"` block those reads and unset secret environment variables for sandboxed commands. `"mode": "mask"` goes further: the command sees a placeholder and the sandbox proxy injects the real value only for requests to hosts you list, so tools keep authenticating without ever holding the secret (requires `network.tlsTerminate`). DevIntern leaves this to you because denying `~/.ssh` breaks SSH-based git pushes, and the right trade-off depends on your remotes and threat model. - `sandbox.network.deniedDomains`: block specific domains even when the broad allow-all is in place, for example known exfiltration endpoints. - Narrower `AGENT_SANDBOX_ALLOWED_DOMAINS`: replacing allow-all with a real allowlist (model API, package registries, your git host) is the single biggest hardening step, at the cost of the first run failing until the list is complete. - `sandbox.filesystem.denyRead`: block reads of specific paths (private repos checked out elsewhere, secret stores) while keeping the rest of the policy. - `sandbox.allowAppleEvents` (macOS): off by default and DevIntern keeps it off; enabling it lets sandboxed commands script other applications, which removes much of the isolation. Only relevant if a task genuinely needs `open` or `osascript`. - `sandbox.enableWeakerNestedSandbox` (Linux): needed only when the agent itself runs inside an unprivileged container; weakens isolation, so enable it only when the container already provides the boundary. - Additional `excludedCommands`: if a tool your tasks rely on is incompatible with the sandbox (the upstream docs mention `watchman`-based `jest` runs and some Go CLIs), adding an exclusion in your project settings is the intended mechanism. Each entry is a hole in the boundary, so keep the list short. One structural note: entries you add in project settings can widen the policy (that is by design upstream); organization-managed Claude Code settings can lock this down with `allowManagedDomainsOnly` and related keys if you need central control. ### When a sandbox wrapper is the wrong tool: isolate at the machine level Some workloads cannot run under any of the providers above. The iOS Simulator and Xcode builds need system Mach services, code signing daemons, and often a GUI session; Unity and other game-engine builds need GPU access and licensing daemons; device testing needs real hardware attached. The microVM providers do not help either: their guests are Linux, so macOS-only tooling cannot run inside them. For these cases, keep `AGENT_SANDBOX=none` and move the isolation boundary from the process to the machine: - **A dedicated VM.** On Apple Silicon, a macOS VM (for example [Tart](https://tart.run) or UTM) runs Xcode and the iOS Simulator with full fidelity; snapshot the VM before agent runs and roll back afterwards. For Unity batch builds, a Windows or Linux VM works the same way. - **A separate machine or cloud runner.** A spare Mac mini, an EC2 Mac instance, or a hosted macOS provider for Apple toolchains; any throwaway cloud VM for the rest. - **DevIntern runs there unchanged.** Install the tool on that machine, keep the project's `.devintern-code/.env` there, and run one-shot tasks or a `devintern worker` daemon exactly as you would locally. - **Scope credentials to the machine.** Use a machine user or deploy key for git, least-privilege tracker and API tokens, and no personal sign-ins beyond the agent login. The isolation then comes with a clear recovery path: if a run goes wrong, roll back the snapshot and rotate only that machine's credentials. This is not a fallback of last resort. A snapshotted VM gives you a stronger guarantee than filesystem confinement (the whole disk state is disposable), at the cost of setup effort and hardware. ## Read-only analysis runs The internal analysis-only spawns, feasibility/clarity check and story point estimation, never write to your repository. On harnesses with native read-only or plan-mode enforcement (`claude-code`, `codex`, `cursor`, `grok`, `opencode`), devintern runs them through that mode automatically and never combines it with a permission-skip flag. Harnesses without native enforcement (for example `antigravity`, `deepseek`, `cline`, `goose`, `kilo-code`, `kimi`, `qwen`) fall back to the previous unattended behavior for these two spawns only; your main implementation run is unaffected either way. If a constrained analysis run errors out, devintern retries it once in the default mode automatically. There's no flag to configure this; it's automatic based on what the harness supports. ```bash # Optional: extend the tool allowlist for read-only analysis runs (comma-separated, # harness tool naming), e.g. to allow an MCP server during the clarity check # AGENT_ANALYSIS_ALLOWED_TOOLS=mcp__notion,mcp__figma__get_design_context ``` `AGENT_ANALYSIS_ALLOWED_TOOLS` only applies when the resolved harness supports read-only mode; it has no effect otherwise. ## CLI Updates On startup, a globally installed `devintern` checks the npm registry (at most once per day) for a newer `@getdevintern/code` version. | Mode | Behavior | | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | Interactive terminal | Offers an update prompt (`Update? (Y/n)`). Accepting installs the new version and re-runs your command. | | Non-interactive (CI, workers, scripts, piped stdin) | **Skips install** (safe default). Prints a one-line notice at most once per check window. | | Opt-out | `DEVINTERN_NO_UPDATE=1` or `--no-update` | | Opt-in auto-install (including non-interactive) | `DEVINTERN_AUTO_UPDATE=1` | Only global npm or bun installs are updated. Monorepo checkouts, `bun link`, and local project `node_modules` installs are left alone. To upgrade immediately without waiting for the prompt or notice, reinstall globally with the package manager you installed with: ```bash bun install -g @getdevintern/code@latest # or npm install -g @getdevintern/code@latest ``` Update-check state (last check time, seen version) is cached per package in `~/.devintern/update-check.json`; delete that file to force a fresh registry lookup on the next run. ## Troubleshooting **"Missing required environment variables"** - Ensure `.env` file exists in `.devintern-code/` or your current directory - Check that variable names match exactly (case-sensitive) **"Not in a git repository"** - Run `git init` if starting a new project - Or use `--no-git` flag to skip git operations **"Claude CLI not found"** / **" CLI not found"** - Install your AI agent CLI and ensure it is on your `PATH` - Or specify path: `--agent-path /path/to/claude` (or set `AGENT_CLI_PATH`) **"Unknown agent harness"** - Check `AGENT_HARNESS` spelling (use kebab-case, e.g. `claude-code`, `grok`, `deepseek`) - The error lists every valid harness name; pick one from that list --- # Sentry Auto-fixes The workspace worker can poll one or more Sentry projects for unresolved error groups and run actionable errors through the normal fix pipeline: isolated worktree, coding agent, tests, commit, and pull request. ## Configure projects in `workspace.toml` Add one `[[error_monitors]]` entry per Sentry project. Every entry maps to the repository that owns the code, so a multi-repo worker never has to guess where an error should be fixed. ```toml [[error_monitors]] id = "api-production" provider = "sentry" repo = "backend" team = "platform" # optional; must match a [[teams]] name organization = "acme" project = "api" query = "environment:production level:error" poll_interval = 60 min_occurrences = 5 max_per_tick = 3 comment_on_action = true env_file = "env/sentry-api.env" [[error_monitors]] id = "web-production" provider = "sentry" repo = "frontend" organization = "acme" project = "web" env_file = "env/sentry-web.env" ``` `repo` may be omitted only when the workspace has exactly one `[[repos]]` entry. In a multi-repo workspace it is required. `team` is optional and lets the fix run inherit that team's environment and identity in addition to the repository environment. Each source is independent: use a separate `env_file` or `[error_monitors.env]` table when projects need different credentials. ```bash # env/sentry-api.env SENTRY_AUTH_TOKEN=sntrys_... ``` ```toml [[error_monitors]] id = "internal-api" provider = "sentry" repo = "backend" organization = "acme" project = "api" base_url = "https://sentry.internal.example" [error_monitors.env] SENTRY_AUTH_TOKEN = "sntrys_..." ``` Do not put a Sentry DSN here. A DSN sends events into Sentry; polling issues requires an auth token plus the organization and project slugs. Create a personal auth token with `event:write` access. When `comment_on_action = true`, DevIntern leaves a short comment on the Sentry issue after a remediation run finishes successfully or fails. Comments are best effort and never change the run outcome or the issue status. Credential precedence, from lowest to highest, is: process environment, workspace `.env`, repo `env_file`, `[repos.env]`, team credentials, the error monitor's `env_file`, then `[error_monitors.env]`. This allows one worker to serve teams and projects whose tokens differ. ## Behavior An error is eligible when it meets `min_occurrences` (default `5`) and includes a title plus a culprit, exception type, or filename. At most `max_per_tick` (default `3`) errors are dispatched per poll. `poll_interval` defaults to `[defaults].poll_interval`. Because the watcher has already applied those actionability checks and supplied concrete runtime evidence, Sentry runs skip the generic task feasibility assessment and proceed directly to implementation. Handled issue IDs are stored in the workspace database under a source key that includes the provider and configured source `id`. That prevents collisions between Sentry projects. A failed fix is not automatically repeated; a run deferred because the repo or agent capacity is busy is released and retried on a later poll. Runs are recorded with an `error_monitor` origin rather than as tracker tasks. If the worker restarts during a run, startup recovery marks the interrupted run failed locally without trying to fetch its synthetic identifier from Jira or another task tracker. The provider contract is shared by all error monitors. Sentry is the first adapter; adding Datadog support does not require another polling, deduplication, or workspace-routing implementation. `[[error_monitors]]` changes are validated by live reload but require a worker restart because clients and credentials are startup-scoped. --- # Running the Worker Unattended The [worker](./worker.md) is the unattended path for @devintern/code. It stays running, picks up ready tasks, watches the agent's pull requests, and receives instant events through the [relay](./relay.md). You do not need a separate scheduler around the CLI. ```bash devintern worker init devintern worker ``` `worker init` writes a 1-repo [workspace](./workspaces.md), stores the ready-tasks query, checks any automation license (Supporter, Team, or Business), and offers verified relay pairing through the central DevIntern AI App (`@mention` handling on any PR, with `GITHUB_TOKEN` retained for local API access). GitHub pairing is enabled only after the relay verifies the App installation and requested repository; skipped pairing is reminded in the summary. The wizard can also generate a user-level systemd unit (Linux) or launchd agent (macOS). Opening http://localhost:4400 is how you know it worked. Air-gapped/no-relay installations use the separate customer-owned App workflow. Keep the worker running and use [working windows](#working-windows-quiet-hours) when you want to control when it may pick up new tasks. ## Requires an automation license Unattended execution (the worker or any CI environment) requires an **automation license** (Supporter, Team, or Business). When @devintern/code detects an automated context but finds no matching license, the run fails immediately with: ``` ❌ License check failed Automated execution detected (CI / systemd / cron) but no automation license was found. ``` Set `LICENSE_KEY` in the workspace `.env` (or as an `Environment=` entry in a unit file) to an automation license key from [devintern.com/account](https://devintern.com/account). Interactive runs (`devintern PROJ-123` from your terminal) are unaffected and do **not** require a license. @devintern/code is free to use interactively under the FSL license. ## Working windows (quiet hours) The worker can limit new-task pickup from your tracker to wall-clock windows you choose — for example, only at night or only outside working hours. Configure them in `[worker.schedule]` in `workspace.toml` and restart the worker: ```toml [worker.schedule] active = ["22:00-06:00"] # drain ready tasks only during these windows (local time) blocked = ["12:00-13:00"] # optional: stay quiet during lunch even inside an active window timezone = "" # optional IANA name ("America/New_York"); blank = machine local time catch_up_missed = true # drain once at startup when a whole window elapsed unused ``` How it behaves: - **Only new-task pickup is gated.** The detect → evaluate → execute drain of the fleet query pauses; review replies, @mentions, recurring automations, relay events, and everything else continue exactly as before. - **In-flight tasks finish.** Execution is sequential: once a task has been picked up it runs to completion even if its window closes mid-run. Nothing is killed at the window edge. - **Multiple windows union.** `active = ["06:00-09:00", "18:00-23:00"]` opens two drain periods per day. Windows may cross midnight (`22:00-06:00`). A start equal to the end is rejected. - **Blocked wins over active.** Overlapping entries resolve toward staying quiet — the safe direction for spend. - **No cursor movement while paused.** Ticks that land inside a quiet window neither query the tracker nor advance cursors, so anything created overnight is detected on the first tick after the window opens. ### Timezones and DST Windows are defined in **wall-clock time**. With no `timezone` set (the default), they follow the worker machine's local time, so "nights" mean _its_ nights. Set any IANA name (for example `timezone = "Europe/Berlin"`) to pin the schedule independent of where the daemon runs; the resolved zone is printed in the startup banner and shown on the dashboard. Daylight-saving transitions shift real window duration by up to an hour because windows track the clock, not absolute time: - **Spring forward**: local times that do not exist snap forward to the next valid moment — a `02:30` start begins within about half an hour of the lost hour instead of never firing. - **Fall back**: a wall time occurring twice is evaluated on its second (standard-time) pass. ### Missed windows and catch-up If the laptop slept through an entire active window (`catch_up_missed = true`, the default), the next worker start drains once immediately instead of waiting for the next window. The check compares the persisted timestamp of the last executed drain against the most recent fully elapsed window; set `catch_up_missed = false` if you would rather skip strictly to the next scheduled one. Worker uptime is unaffected either way: catch-up triggers only on startup. ### Run now, without editing the schedule `devintern worker run-now` asks the running worker for one immediate drain, ignoring the windows: ```bash devintern worker run-now devintern worker run-now --workspace /path/to/workspace.toml ``` The command writes a `.run-now` marker into the workspace home; the worker consumes it on its next poll tick (within `[defaults].poll_interval`, 60 seconds by default), drains, and removes the marker. Logs announce the manual run; the dashboard shows it as pending until served. ### Seeing the current state - The startup banner lists the windows, the timezone, and whether pickup is currently open. - Every open/close flip is logged exactly once: `🌙 [schedule] outside the working window … ` / `☀️ [schedule] working window opened …`. - The [dashboard](./dashboard.md) header shows the active state plus when the window next opens or closes; `/api/worker` exposes the same snapshot as JSON (`schedule`). Scheduled story-point estimation is a worker job too — see `[[estimations]]` in [Story Points Estimation](./story-points-estimation.md). No CLI runs on timers. ### Keeping unattended runs healthy #### Pin `PATH` so the `bun` shebang resolves The `devintern` binary is a `#!/usr/bin/env bun` script, so it needs **`bun` on `PATH`** to run. Services launched by init systems start with a minimal `PATH` that usually excludes wherever your version manager (mise, asdf, nvm) installed Bun, and then fail with `bun: command not found`. Pin `PATH` explicitly in the `[Service]` section, listing the directory that contains `bun` (and `devintern`): ```ini [Service] Environment="PATH=/home/youruser/.local/bin:/home/youruser/.local/share/mise/installs/bun/1.3.2/bin:/usr/local/bin:/usr/bin" ``` Confirm the path with `dirname "$(which bun)"`. #### Running as a user service (no root) Instead of system units under `/etc/systemd/system` (which need `sudo`), you can run entirely as your own user with **`systemctl --user`**: no root, and the unit can read your `~/.ssh` and version-manager installs directly. Place the unit in `~/.config/systemd/user/`, drop the `User=` line, and manage it with `systemctl --user enable --now .service`. To keep user services running after you log out, enable lingering once: ```bash loginctl enable-linger "$USER" ``` `devintern worker init` already writes a user-level systemd unit (Linux) or launchd agent (macOS) for the resident worker. #### Git push under automation If your repo's remote is SSH (`git@github.com:...`), the unattended run needs the SSH key reachable without an interactive agent. The cleanest approach is a `~/.ssh/config` host entry pointing the host at the right key: plain `git push` then resolves it (no `GIT_SSH_COMMAND` needed): ``` Host github.com IdentityFile ~/.ssh/your_key ``` A `--user` service inherits your `$HOME` and reads this automatically; a system service with `User=` reads that user's `~/.ssh`. Alternatively, use an HTTPS remote with a `GITHUB_TOKEN`. #### Cleaning up processes the agent leaves running While working a task, the AI agent often starts long-running processes to verify its changes: dev servers (`npm run dev`, `vite`), watchers, `docker compose up`, and so on. If the agent does not stop them, they would otherwise outlive the run and pile up across every execution. @devintern/code prevents this. Each agent is launched in its own process group, and the entire group (the agent plus anything it spawned) is torn down when the run ends, times out, or is interrupted — the same however the worker is launched (systemd, launchd, a plain terminal), so you do not need to do anything to enable it: 1. **In-process reaping (all platforms).** @devintern/code signals the whole process group on completion, on timeout, and on `SIGINT` / `SIGTERM` / `SIGHUP`. This protects launch methods without init-level cleanup of their own. 2. **systemd cgroup cleanup (Linux, bonus).** A systemd service confines all of its processes to a unit cgroup, and the default `KillMode=control-group` reaps that entire cgroup when the unit deactivates. This catches even processes that fully daemonize (call `setsid` themselves) and escape the process group. #### Failure feedback on the task tracker A failed run never ends silently. When processing a task fails after it was moved to "In Progress" — an agent timeout, a usage limit, a crash, or the process being killed by `SIGTERM`/`SIGINT` (for example when a machine powers off) — @devintern/code posts a comment on the ticket explaining that no pull request was created, the reason for the failure, and where partial work may live (the `feature/` branch or a git stash). The ticket is also moved back to its To Do status so the next pickup can retry it. The failure comment will not cause a retry loop: posting it does bump the tracker's update stamp, but the retry gate ignores the harness's own comments and records the attempt, so the ticket is only re-run after you edit the description, post your own comment, or delete the failure comment (see [worker polling](./worker.md#re-running-a-task)). That covers graceful stops. When the worker itself dies mid-task (power cut, crash, `kill -9`), no comment could be posted at the time — so on its next startup the worker detects the runs left in flight, comments on their tickets with the same failure explanation, and moves them back to To Do. Tickets that moved on after the crash and orphans older than `WORKER_ORPHAN_MAX_AGE_HOURS` (default 168) are left alone. See [Interrupted runs are recovered on startup](./worker.md#interrupted-runs-are-recovered-on-startup). Pass `--skip-comments` to disable all tracker comments, including failure feedback. ---