diff --git a/docs/code/automated-task-processing.md b/docs/code/automated-task-processing.md index 75d316b..d2686f9 100644 --- a/docs/code/automated-task-processing.md +++ b/docs/code/automated-task-processing.md @@ -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 @@ -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 @@ -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 ``` @@ -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 @@ -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 diff --git a/docs/code/usage.md b/docs/code/usage.md index 19aa302..988cd4f 100644 --- a/docs/code/usage.md +++ b/docs/code/usage.md @@ -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 diff --git a/packages/code/CHANGELOG.md b/packages/code/CHANGELOG.md index 9a38cb2..8aa8d8a 100644 --- a/packages/code/CHANGELOG.md +++ b/packages/code/CHANGELOG.md @@ -6,7 +6,7 @@ - **Hook-fix verification after a successful push**: if HEAD already matches `origin/`, 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 diff --git a/packages/code/USAGE.md b/packages/code/USAGE.md index 83e1a33..9bc4cd3 100644 --- a/packages/code/USAGE.md +++ b/packages/code/USAGE.md @@ -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 @@ -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 @@ -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 diff --git a/packages/code/src/index.ts b/packages/code/src/index.ts index 965f285..27d0b3c 100755 --- a/packages/code/src/index.ts +++ b/packages/code/src/index.ts @@ -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 ", "Target branch for pull request", "main") + .option( + "--pr-target-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 ", "Maximum iterations for auto-review loop", "5") .option("--skip-comments", "Skip posting comments to the task tracker (for testing)") @@ -2143,6 +2147,14 @@ async function main(): Promise { 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..."); diff --git a/packages/code/src/lib/utils.ts b/packages/code/src/lib/utils.ts index ea506e4..074a008 100644 --- a/packages/code/src/lib/utils.ts +++ b/packages/code/src/lib/utils.ts @@ -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 @@ -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 @@ -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 { - 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; } @@ -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 */ @@ -797,6 +820,10 @@ export class Utils { ) { return preferredBranch; } + + if (await Utils.remoteBranchExists(preferredBranch, { cwd: options?.cwd })) { + return preferredBranch; + } } return Utils.getMainBranchName(options); diff --git a/packages/code/tests/default-branch-detection.test.ts b/packages/code/tests/default-branch-detection.test.ts index 10c8760..7d11c91 100644 --- a/packages/code/tests/default-branch-detection.test.ts +++ b/packages/code/tests/default-branch-detection.test.ts @@ -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");