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
7 changes: 6 additions & 1 deletion .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,18 @@ jobs:
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
# Pass the commit message through the environment, never interpolate it
# directly into the shell — a message containing backticks / $() / ;
# would otherwise break the script or allow command injection in a job
# that holds CLOUDFLARE_API_TOKEN.
COMMIT_MESSAGE: ${{ github.event.head_commit.message || 'Deploy' }}
run: |
BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}"
npx wrangler pages deploy docs/.vitepress/dist \
--project-name=protolabs-docs \
--branch="$BRANCH" \
--commit-hash="${GITHUB_SHA}" \
--commit-message="${{ github.event.head_commit.message || 'Deploy' }}"
--commit-message="${COMMIT_MESSAGE:-Deploy}"

- name: Comment PR preview URL
if: github.event_name == 'pull_request'
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/deploy-site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,18 @@ jobs:
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
# Pass the commit message through the environment, never interpolate it
# directly into the shell — a message containing backticks / $() / ;
# would otherwise break the script or allow command injection in a job
# that holds CLOUDFLARE_API_TOKEN.
COMMIT_MESSAGE: ${{ github.event.head_commit.message || 'Deploy' }}
run: |
BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}"
npx -y wrangler pages deploy site \
--project-name=automaker \
--branch="$BRANCH" \
--commit-hash="${GITHUB_SHA}" \
--commit-message="${{ github.event.head_commit.message || 'Deploy' }}"
--commit-message="${COMMIT_MESSAGE:-Deploy}"

- name: Comment PR preview URL
if: github.event_name == 'pull_request'
Expand Down
12 changes: 8 additions & 4 deletions apps/server/src/services/auto-mode-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1214,11 +1214,15 @@ export class AutoModeService {
continue;
}

// Start feature execution in background
// Start feature execution in background.
// useWorktrees: default to true so agents always run in an isolated
// worktree, never the main checkout. (Read-only features could skip
// worktrees, but isolation-by-default is the safe choice — a stray
// write/install in the main checkout corrupts the running server.)
this.executeFeature(
projectPath,
nextFeature.id,
nextFeature.executionMode !== 'read-only', // useWorktrees
true, // useWorktrees — always isolate
true
).catch((error) => {
logger.error(`Feature ${nextFeature.id} error:`, error);
Expand Down Expand Up @@ -1316,7 +1320,7 @@ export class AutoModeService {
async executeFeature(
projectPath: string,
featureId: string,
useWorktrees = false,
useWorktrees = true,
isAutoMode = false,
providedWorktreePath?: string,
options?: ExecuteFeatureOptions
Expand Down Expand Up @@ -1699,7 +1703,7 @@ export class AutoModeService {
/**
* Resume a feature (continues from saved context)
*/
async resumeFeature(projectPath: string, featureId: string, useWorktrees = false): Promise<void> {
async resumeFeature(projectPath: string, featureId: string, useWorktrees = true): Promise<void> {
if (this.runningFeatures.has(featureId)) {
const existing = this.runningFeatures.get(featureId);
const runtime = existing ? Math.floor((Date.now() - existing.startTime) / 1000) : 0;
Expand Down
102 changes: 102 additions & 0 deletions apps/server/src/services/auto-mode/execution-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import { existsSync, readFileSync } from 'node:fs';
import { ProviderFactory } from '../../providers/provider-factory.js';
import { TracedProvider } from '../../providers/traced-provider.js';
import { simpleQuery } from '../../providers/simple-query-service.js';
import {
buildFreshEyesReviewPrompt,
FRESH_EYES_REVIEW_SYSTEM_PROMPT,
parseFreshEyesVerdict,
} from '@protolabsai/prompts';
import { StreamObserver } from '../stream-observer-service.js';
import { getWorkflowSettings, getEffectivePrBaseBranch } from '../../lib/settings-helpers.js';
import { setFeatureContext } from '@protolabsai/error-tracking';
Expand Down Expand Up @@ -1391,6 +1396,20 @@ Output the branch name only.`,
logger.warn('Failed to record learnings:', learningError);
}

// Local antagonistic review of the worktree diff BEFORE the git workflow
// commits/pushes/opens the PR (#3799). Reviews the agent's first cut and,
// on a BLOCK/CONCERN verdict, runs one fix iteration in the worktree — so
// we open a reviewed, iterated PR instead of pushing the first cut
// unreviewed. Fail-open: never blocks shipping.
await this.reviewAndIterateWorktreeDiff(
workDir,
projectPath,
feature,
abortController,
modelResult.model,
modelResult.providerId
);

// Run git workflow (commit, push, PR) if enabled
// Read-only features skip the git workflow entirely — no commit, push, or PR.
let gitWorkflowResult: Awaited<
Expand Down Expand Up @@ -2371,6 +2390,89 @@ Complete the pipeline step instructions above. Review the previous work and appl
return prompt;
}

/**
* Local antagonistic review of the worktree diff, run AFTER the agent finishes
* but BEFORE the git workflow commits/pushes/opens the PR (#3799). Reviews the
* agent's diff with a fresh-eyes pass; on a BLOCK/CONCERN verdict, re-invokes
* the agent once to address the findings. Fail-open: any error here just logs
* and proceeds to the git workflow — the local review must never block shipping.
*/
private async reviewAndIterateWorktreeDiff(
workDir: string,
projectPath: string,
feature: Feature,
abortController: AbortController,
model: string,
providerId: string | undefined
): Promise<void> {
if (feature.executionMode === 'read-only') return;
const MAX_BUF = 64 * 1024 * 1024;
try {
// Stage everything (incl. new files) so the diff is complete. The git
// workflow re-stages before committing, so staging here is harmless.
const GIT_TIMEOUT = 120_000;
await execAsync('git add -A', { cwd: workDir, maxBuffer: MAX_BUF, timeout: GIT_TIMEOUT });
const { stdout: diff } = await execAsync('git diff --cached', {
cwd: workDir,
maxBuffer: MAX_BUF,
timeout: GIT_TIMEOUT,
});
if (!diff || diff.trim().length === 0) {
logger.info(`[LocalReview] ${feature.id}: no diff to review`);
return;
}

const reviewUserPrompt = buildFreshEyesReviewPrompt({
prDiff: diff,
featureTitle: feature.title ?? feature.id,
featureDescription: feature.description ?? '',
});
const reviewResult = await simpleQuery({
prompt: `${FRESH_EYES_REVIEW_SYSTEM_PROMPT}\n\n${reviewUserPrompt}`,
model,
cwd: workDir,
maxTurns: 1,
allowedTools: [],
});
const verdict = parseFreshEyesVerdict(reviewResult.text || '');
logger.info(
`[LocalReview] ${feature.id}: verdict ${verdict.verdict} — ${verdict.reasoning.slice(0, 200)}`
);

if (verdict.verdict === 'PASS') return;

// One fix iteration addressing the review findings.
const fixPrompt =
`A pre-PR code review of your changes returned ${verdict.verdict}. ` +
`Address the following findings by editing files in this worktree. ` +
`Make only the changes needed to resolve them — no unrelated changes.\n\n` +
`REVIEW FINDINGS:\n${verdict.reasoning}`;
logger.info(`[LocalReview] ${feature.id}: running one fix iteration (${verdict.verdict})`);
await this.runAgent(
workDir,
feature.id,
fixPrompt,
abortController,
projectPath,
undefined,
model,
{
projectPath,
maxTurns: 30,
providerId,
phase: 'REVIEW',
branchName: feature.branchName ?? null,
}
);
logger.info(`[LocalReview] ${feature.id}: fix iteration complete`);
} catch (err) {
logger.warn(
`[LocalReview] ${feature.id}: review/iterate failed, proceeding to git workflow:`,
err
);
}
}

/**
* Run the agent for a feature or pipeline step.
*/
Expand Down
7 changes: 6 additions & 1 deletion apps/server/src/services/project-orchestration-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,12 @@ export async function orchestrateProjectFeatures(
projectSlug,
milestoneSlug: milestone.slug,
phaseSlug: phase.name,
workflow: phase.workflow ?? options.defaultWorkflow,
// Project phases are code-implementation work — default to the full
// 'standard' pipeline (INTAKE→PLAN→EXECUTE→REVIEW→MERGE→DEPLOY).
// Without this, an unset workflow falls into the match-rule scoring,
// which mis-matched phases to read-only workflows like changelog-digest
// (no PR, no worktree → agents ran in main). See #3788/#3793.
workflow: phase.workflow ?? options.defaultWorkflow ?? 'standard',
});

result.phaseFeatureMap[phaseKey] = feature.id;
Expand Down
12 changes: 9 additions & 3 deletions apps/server/src/services/worktree-lifecycle-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -771,10 +771,16 @@ export class WorktreeLifecycleService {
* and build tooling. These are symlinked from the main project into worktrees
* after creation so that external projects' toolchains work correctly.
*
* Only top-level directories are symlinked. Nested node_modules (inside
* workspace packages) are resolved transitively through the root symlink.
* Only top-level directories are symlinked.
*
* NOTE: node_modules is deliberately NOT symlinked. Sharing the host's
* node_modules meant an agent running `npm install` in a worktree (e.g. to add
* a workspace package) rewrote the host's deps and broke the running server.
* Worktrees now install their own node_modules via the worktree-init script
* (`npm ci`), keeping each worktree isolated. dist/build are still symlinked
* since agents read them and don't mutate them.
*/
const BUILD_ARTIFACT_DIRS = ['node_modules', 'dist', 'build', '.next', '.nuxt', 'out'] as const;
const BUILD_ARTIFACT_DIRS = ['dist', 'build', '.next', '.nuxt', 'out'] as const;

/**
* Symlink gitignored build artifacts from the main project into a worktree.
Expand Down
16 changes: 8 additions & 8 deletions docs/reference/auto-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,14 +205,14 @@ interface AutoModeConfig {

Settings read from `workflowSettings` in `.automaker/settings.json`:

| Setting | Description |
| --------------------- | -------------------------------------------- |
| `agentExecutionModel` | Primary model for agent execution |
| `maxConcurrency` | Max parallel agents (capped at system limit) |
| `useWorktrees` | Enable per-feature git worktrees |
| `autoLoadClaudeMd` | Auto-inject CLAUDE.md into agent context |
| `mcpServers` | MCP server config passed to Claude SDK |
| `planningMode` | Enable plan approval gating |
| Setting | Description | Default |
| --------------------- | -------------------------------------------- | ------- |
| `agentExecutionModel` | Primary model for agent execution | — |
| `maxConcurrency` | Max parallel agents (capped at system limit) | — |
| `useWorktrees` | Enable per-feature git worktrees | `true` |
| `autoLoadClaudeMd` | Auto-inject CLAUDE.md into agent context | — |
| `mcpServers` | MCP server config passed to Claude SDK | — |
| `planningMode` | Enable plan approval gating | — |

## Prometheus Metrics

Expand Down
Loading
Loading