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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions docs/code/automated-task-processing.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: "Automated Task Processing"
description: "Drain your backlog continuously with the worker daemon, or schedule @devintern/code via systemd timers or cron"
section: "Server Automation"
order: 5
dateModified: 2026-07-21
dateModified: 2026-08-19
---

# Automated Task Processing
Expand All @@ -28,7 +28,7 @@ Set `LICENSE_KEY` in your project's `.devintern-code/.env` (or as an `Environmen

## systemd timers

A systemd job is a pair of unit files: a one-shot `.service` that runs `devintern`, and a `.timer` that triggers it on a schedule.
A systemd job is a pair of unit files: a one-shot `.service` that runs `devintern`, and a `.timer` that triggers it on a schedule. Omit `--pr-target-branch` unless you intentionally want PRs against a non-default branch — the CLI detects `main`, `master`, or a custom default from the remote.

### Every 10 minutes: process Intern-labeled tasks

Expand All @@ -47,8 +47,7 @@ WorkingDirectory=/path/to/your/project
ExecStart=/usr/local/bin/devintern \
--query 'statusCategory = "To Do" AND sprint in openSprints() AND labels IN (Intern) ORDER BY created DESC' \
--max-turns 500 \
--create-pr \
--pr-target-branch master
--create-pr
StandardOutput=journal
StandardError=journal
```
Expand Down Expand Up @@ -131,7 +130,7 @@ If you're on a system without systemd, the same three jobs work as crontab entri

```bash
# Process tasks labeled "Intern" in open sprints every 10 minutes
*/10 * * * * cd /path/to/your/project && devintern --query 'statusCategory = "To Do" AND sprint in openSprints() AND labels IN (Intern) ORDER BY created DESC' --max-turns 500 --create-pr --pr-target-branch master >> /tmp/devintern-cron.log 2>&1
*/10 * * * * cd /path/to/your/project && devintern --query 'statusCategory = "To Do" AND sprint in openSprints() AND labels IN (Intern) ORDER BY created DESC' --max-turns 500 --create-pr >> /tmp/devintern-cron.log 2>&1

# Process AutoImpl-labeled tasks every hour
0 * * * * cd /path/to/your/project && devintern --query 'status = "To Do" AND labels IN (AutoImpl)' --create-pr >> /tmp/devintern-cron.log 2>&1
Expand Down Expand Up @@ -196,13 +195,12 @@ For Linear (`TASK_TRACKER=linear`), swap JQL for a JSON `IssueFilter`. Wrap the
ExecStart=/usr/local/bin/devintern \
--query '{"labels":{"name":{"eq":"intern"}}}' \
--max-turns 500 \
--create-pr \
--pr-target-branch master
--create-pr
```

```bash
# cron: process "intern"-labeled Linear issues every 10 minutes
*/10 * * * * cd /path/to/your/project && devintern --query '{"labels":{"name":{"eq":"intern"}}}' --max-turns 500 --create-pr --pr-target-branch master >> /tmp/devintern-cron.log 2>&1
*/10 * * * * cd /path/to/your/project && devintern --query '{"labels":{"name":{"eq":"intern"}}}' --max-turns 500 --create-pr >> /tmp/devintern-cron.log 2>&1

# cron: process high-priority issues assigned to me every hour
0 * * * * cd /path/to/your/project && devintern --query '{"assignee":{"isMe":{"eq":true}},"priority":{"lte":2}}' --create-pr >> /tmp/devintern-cron.log 2>&1
Expand Down
2 changes: 1 addition & 1 deletion docs/code/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ devintern MYAPP-456
- 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` (defaults to `main`)
- 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
Expand Down
2 changes: 1 addition & 1 deletion packages/code/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

- **Hook-fix verification after a successful push**: if HEAD already matches `origin/<branch>`, skip the pre-push `--dry-run` (and the follow-up no-op `git push`). Re-running the hook suite can flake — a 30s CLI test timeout previously aborted PR creation for a branch that was already on the remote, with a misleading "didn't amend" error
- **CLI argument tests no longer hang on a live tracker host**: parse-only CLI tests point Jira, Linear, and Trello traffic at a closed local port and disable fetch retries (`DEVINTERN_FETCH_MAX_RETRIES=0`), so a slow remote lookup cannot burn the 30s bun timeout and fail pre-push
- **Default branch detection**: repository automations now ask the remote for its authoritative default branch before checkout or fetch operations, avoiding failed `master` attempts for repositories whose default is `main` (and supporting custom default branch names). Cached `origin/HEAD` remains an offline fallback
- **Default branch detection**: repository automations now ask the remote for its authoritative default branch before checkout or fetch operations, avoiding failed `master` attempts for repositories whose default is `main` (and supporting custom default branch names). Cached `origin/HEAD` remains an offline fallback. An explicit `--pr-target-branch master` (or `main`) that is missing on the remote now falls back the same way, so copied cron examples no longer `git fetch` a doomed ref before switching to the real default
- **Structured agent responses**: feasibility checks now accept the valid bare JSON commonly returned by Codex instead of requiring a Markdown code fence. Feasibility, estimation, and auto-review share brace-aware extraction that also tolerates narration and ignores unrelated braces in prose

## [2.3.2] - 2026-08-18
Expand Down
6 changes: 3 additions & 3 deletions packages/code/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ devintern MYAPP-456
- GitHub: Requires `GITHUB_TOKEN` for personal CLI PR creation (see ENV_SETUP.md). A team GitHub App can also create PRs; `@mention` matching on any PR needs the App.
- Bitbucket: Requires `BITBUCKET_TOKEN` (`Repositories: Write`), workspace auto-detected from git remote
- Can be enabled with `--create-pr` flag
- Target branch can be specified with `--pr-target-branch` (defaults to 'main')
- 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

### Git Requirements

Expand Down Expand Up @@ -435,7 +435,7 @@ You can set up automated task processing using cron jobs. This is useful for con
```bash
# Example: Process tasks labeled "Intern" in open sprints every 10 minutes
# Add to crontab (run: crontab -e)
*/10 * * * * cd /path/to/your/project && devintern --jql 'statusCategory = "To Do" AND sprint in openSprints() AND labels IN (Intern) ORDER BY created DESC' --max-turns 500 --create-pr --pr-target-branch master >> /tmp/devintern-cron.log 2>&1
*/10 * * * * cd /path/to/your/project && devintern --jql 'statusCategory = "To Do" AND sprint in openSprints() AND labels IN (Intern) ORDER BY created DESC' --max-turns 500 --create-pr >> /tmp/devintern-cron.log 2>&1

# Example: Process assigned tasks every hour
0 * * * * cd /path/to/your/project && devintern --jql 'assignee = currentUser() AND status = "To Do" AND labels IN (AutoImpl)' --create-pr >> /tmp/devintern-cron.log 2>&1
Expand All @@ -450,7 +450,7 @@ The same idea works for Linear using a JSON `IssueFilter`. Wrap the JSON in sing

```bash
# Example: Process "intern"-labeled Linear issues every 10 minutes
*/10 * * * * cd /path/to/your/project && devintern --query '{"labels":{"name":{"eq":"intern"}}}' --max-turns 500 --create-pr --pr-target-branch master >> /tmp/devintern-cron.log 2>&1
*/10 * * * * cd /path/to/your/project && devintern --query '{"labels":{"name":{"eq":"intern"}}}' --max-turns 500 --create-pr >> /tmp/devintern-cron.log 2>&1

# Example: Process high-priority Linear issues assigned to you every hour
0 * * * * cd /path/to/your/project && devintern --query '{"assignee":{"isMe":{"eq":true}},"priority":{"lte":2}}' --create-pr >> /tmp/devintern-cron.log 2>&1
Expand Down
14 changes: 13 additions & 1 deletion packages/code/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1333,7 +1333,11 @@ program
.option("--no-auto-commit", "Skip automatic git commit after Agent completes")
.option("--skip-clarity-check", "Skip running Agent for clarity assessment")
.option("--create-pr", "Create pull request after implementation")
.option("--pr-target-branch <branch>", "Target branch for pull request", "main")
.option(
"--pr-target-branch <branch>",
"Target branch for pull request (omitting this uses the repository default branch)",
"main",
)
.option("--auto-review", "Run automatic PR review loop after creating PR (requires --create-pr)")
.option("--auto-review-iterations <number>", "Maximum iterations for auto-review loop", "5")
.option("--skip-comments", "Skip posting comments to the task tracker (for testing)")
Expand Down Expand Up @@ -2143,6 +2147,14 @@ async function main(): Promise<void> {
if (prTargetBranchSource === "default") {
options.prTargetBranch = await Utils.getMainBranchName();
console.log(` Default branch detected as '${options.prTargetBranch}'`);
} else {
const requestedBranch = options.prTargetBranch;
options.prTargetBranch = await Utils.resolveDefaultBranch(requestedBranch);
if (options.prTargetBranch !== requestedBranch) {
console.log(
`⚠️ Target branch '${requestedBranch}' not found on remote, falling back to '${options.prTargetBranch}'`,
);
}
}

console.log("\n📥 Pulling latest changes from remote...");
Expand Down
37 changes: 32 additions & 5 deletions packages/code/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,8 @@ export class Utils {
/**
* Pull latest commits for a branch from `origin`.
*
* Resolves a missing preferred name to the repository default first so a
* stale `master`/`main` request does not `git fetch` a ref the remote lacks.
* Fetches the branch from `origin` when it is not available locally before checkout.
*
* @param branch - Branch to update
Expand Down Expand Up @@ -632,6 +634,12 @@ export class Utils {
};
}

const requestedBranch = branch;
branch = await Utils.resolveDefaultBranch(requestedBranch, { cwd });
if (verbose && branch !== requestedBranch) {
console.log(`⚠️ Branch '${requestedBranch}' not found, trying '${branch}'...`);
}

const currentBranch = await Utils.getCurrentBranch(cwd);

// Switch to target branch if not already on it
Expand Down Expand Up @@ -741,12 +749,22 @@ export class Utils {
*/
static async remoteBranchExists(
branch: string,
options?: { verbose?: boolean; cwd?: string },
options?: {
verbose?: boolean;
cwd?: string;
timeoutMs?: number;
env?: NodeJS.ProcessEnv;
},
): Promise<boolean> {
const result = await Utils.executeGitCommand(
["ls-remote", "--heads", "origin", branch],
options,
);
const result = await Utils.executeGitCommand(["ls-remote", "--heads", "origin", branch], {
...options,
timeoutMs: options?.timeoutMs ?? 5000,
env: {
GIT_TERMINAL_PROMPT: "0",
GCM_INTERACTIVE: "Never",
...options?.env,
},
});
return result.success && result.output.trim().length > 0;
}

Expand Down Expand Up @@ -783,6 +801,11 @@ export class Utils {
/**
* Resolve the repository default branch, honoring a preferred name when present.
*
* The preferred name is kept when it exists locally or on `origin`. Otherwise
* this falls back to {@link getMainBranchName} so callers that pass a stale
* conventional default (`master` on a `main` repo, or the reverse) do not
* issue a doomed `git fetch` for a ref the remote does not have.
*
* @param preferredBranch - Optional branch to prefer when it exists
* @param options - Optional working directory
*/
Expand All @@ -797,6 +820,10 @@ export class Utils {
) {
return preferredBranch;
}

if (await Utils.remoteBranchExists(preferredBranch, { cwd: options?.cwd })) {
return preferredBranch;
}
}

return Utils.getMainBranchName(options);
Expand Down
36 changes: 36 additions & 0 deletions packages/code/tests/default-branch-detection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,42 @@ describe("Default branch detection", () => {
}
});

test("does not fetch master when pullLatestChanges is asked for master on a main-only repo", async () => {
configureOrigin("main");
execSync("git checkout -b feature/test", { cwd: repoDir });

const gitCommands = spyOn(Utils, "executeGitCommand");
try {
const result = await Utils.pullLatestChanges("master", { cwd: repoDir, verbose: true });

expect(result.success).toBe(true);
expect(await Utils.getCurrentBranch(repoDir)).toBe("main");
const commands = gitCommands.mock.calls.map(([args]) => args.join(" "));
expect(
commands.some((command) => command.includes("fetch") && command.includes("master")),
).toBe(false);
} finally {
gitCommands.mockRestore();
}
});

test("keeps a preferred branch that exists only on the remote", async () => {
configureOrigin("main");
execSync("git checkout -b develop", { cwd: repoDir });
execSync("git push origin develop", { cwd: repoDir });
execSync("git checkout main", { cwd: repoDir });
execSync("git branch -D develop", { cwd: repoDir });
execSync("git update-ref -d refs/remotes/origin/develop", { cwd: repoDir });

await expect(Utils.resolveDefaultBranch("develop", { cwd: repoDir })).resolves.toBe("develop");
});

test("falls back when an explicit preferred branch is missing on the remote", async () => {
configureOrigin("main");

await expect(Utils.resolveDefaultBranch("master", { cwd: repoDir })).resolves.toBe("main");
});

test("uses master when remote metadata identifies master", async () => {
configureOrigin("master");

Expand Down
Loading