Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
25f28d7
Fix auto mode concurrency slider not updating when no worktree selected
Feb 4, 2026
2f06db1
Update default Claude Opus model to 4.6
Feb 5, 2026
3d15732
Label Opus as Opus 4.6 in UI
Feb 5, 2026
a47cd04
Add Codex 5.3 model entries
Feb 5, 2026
cf479e8
Add debug endpoint for resolved model proof
Feb 5, 2026
12df9eb
platform: prefer working codex paths; support Volta locations
Feb 6, 2026
0a77f78
Add restart/status scripts for automaker
Feb 6, 2026
2b46a92
Fix quoting in restart-automaker.cmd for cmd.exe
Feb 6, 2026
93ccba2
Fix cmd quoting in restart-automaker.cmd
Feb 6, 2026
36fcb3f
Allow auto mode max concurrency to be set to 0
Feb 6, 2026
efd760f
Add 9-column kanban board: Backlog, Ready, Assigned, In Progress, Blo…
Feb 7, 2026
406bcff
Update Opus model references from 4.5 to 4.6 across source, tests, an…
Feb 7, 2026
49b0507
Add setup prompt template to Automaker docs
Feb 7, 2026
e4d3525
Add worktree auto-creation and update setup template
Feb 7, 2026
ea5bc41
Auto-generate branchName at execution time instead of pre-setting
Feb 7, 2026
3eb497f
Fix setup template: branchName must be null, not pre-set
Feb 7, 2026
c162dfd
Fix auto-mode bypassing maxConcurrency when branchName auto-generated
Feb 7, 2026
72f9e37
Auto-mode picks from Ready only, not Backlog
Feb 7, 2026
bff306f
Document Ready workflow in setup template
Feb 7, 2026
88fc610
Auto-merge verified feature branches back to main
Feb 7, 2026
cdd10d2
Fix auto-mode still picking up pending (backlog) features
Feb 7, 2026
4eb1354
Fix loadPendingFeatures deleting valid dependencies
Feb 7, 2026
3a3ea97
Set startedAt on in_progress features, add auto-merge log and setting…
Feb 7, 2026
474e6f7
Fix timer, merge, and UI issues found during auto-merge testing
Feb 7, 2026
49c78af
Fix unterminated regex in log-parser extractAutoMergeSection
Feb 7, 2026
95ed903
Fix log-parser regex re-broken by agent during auto-mode test
Feb 7, 2026
2a82729
Add failure recovery with retry loop and model escalation for auto-mode
Feb 7, 2026
8235429
Add Failed column to Kanban board for retry-exhausted features
Feb 7, 2026
ac6732d
feat: Add self-review pass before feature completion
Feb 7, 2026
c337154
feat: Add per-feature token usage and duration tracking
Feb 7, 2026
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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ Use `resolveModelString()` from `@automaker/model-resolver` to convert model ali

- `haiku``claude-haiku-4-5`
- `sonnet``claude-sonnet-4-20250514`
- `opus``claude-opus-4-5-20251101`
- `opus``claude-opus-4-6`

## Environment Variables

Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import { createNotificationsRoutes } from './routes/notifications/index.js';
import { getNotificationService } from './services/notification-service.js';
import { createEventHistoryRoutes } from './routes/event-history/index.js';
import { getEventHistoryService } from './services/event-history-service.js';
import { createDebugRoutes } from './routes/debug/index.js';

// Load environment variables
dotenv.config();
Expand Down Expand Up @@ -344,6 +345,7 @@ app.use('/api/pipeline', createPipelineRoutes(pipelineService));
app.use('/api/ideation', createIdeationRoutes(events, ideationService, featureLoader));
app.use('/api/notifications', createNotificationsRoutes(notificationService));
app.use('/api/event-history', createEventHistoryRoutes(eventHistoryService, settingsService));
app.use('/api/debug', createDebugRoutes(settingsService));

// Create HTTP server
const server = createServer(app);
Expand Down
6 changes: 3 additions & 3 deletions apps/server/src/providers/claude-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,9 +349,9 @@ export class ClaudeProvider extends BaseProvider {
getAvailableModels(): ModelDefinition[] {
const models = [
{
id: 'claude-opus-4-5-20251101',
name: 'Claude Opus 4.5',
modelString: 'claude-opus-4-5-20251101',
id: 'claude-opus-4-6',
name: 'Claude Opus 4.6',
modelString: 'claude-opus-4-6',
provider: 'anthropic',
description: 'Most capable Claude model',
contextWindow: 200000,
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/providers/provider-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export class ProviderFactory {
/**
* Get the appropriate provider for a given model ID
*
* @param modelId Model identifier (e.g., "claude-opus-4-5-20251101", "cursor-gpt-4o", "cursor-auto")
* @param modelId Model identifier (e.g., "claude-opus-4-6", "cursor-gpt-4o", "cursor-auto")
* @param options Optional settings
* @param options.throwOnDisconnected Throw error if provider is disconnected (default: true)
* @returns Provider instance for the model
Expand Down
88 changes: 88 additions & 0 deletions apps/server/src/routes/debug/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { Request, Response } from 'express';
import express from 'express';

import { resolveModelString, resolvePhaseModel } from '@automaker/model-resolver';
import { DEFAULT_MODELS } from '@automaker/types';
import type { SettingsService } from '../../services/settings-service.js';

/**
* Debug routes (authenticated)
*
* These endpoints are intended for local verification and troubleshooting.
* Do not return secrets.
*/
export function createDebugRoutes(settingsService: SettingsService) {
const router = express.Router();

/**
* Return the raw configured model keys and their resolved effective model IDs.
*
* This is the authoritative source for "which model will be used" because it uses
* the same resolver as agent runs.
*/
router.get('/resolved-models', async (_req: Request, res: Response) => {
const settings = await settingsService.getGlobalSettings();

const defaultFeatureModelKey = settings.defaultFeatureModel?.model;

const phaseModels = settings.phaseModels || ({} as any);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using as any bypasses type safety. Since settings.phaseModels can be undefined, using the nullish coalescing operator ?? {} is a safer way to provide a default empty object. The properties on phaseModels can then be accessed with optional chaining (e.g., phaseModels?.specGenerationModel), which resolvePhaseModel already handles gracefully for undefined inputs.

Suggested change
const phaseModels = settings.phaseModels || ({} as any);
const phaseModels = settings.phaseModels ?? {};


const specGeneration = resolvePhaseModel(
phaseModels.specGenerationModel,
DEFAULT_MODELS.claude
);
const backlogPlanning = resolvePhaseModel(
phaseModels.backlogPlanningModel,
DEFAULT_MODELS.claude
);
const validation = resolvePhaseModel(phaseModels.validationModel, DEFAULT_MODELS.claude);

// Also show what the legacy "validationModel" / "enhancementModel" shortcuts are set to (if present)
const legacyValidationModelKey = (settings as any).validationModel;
const legacyEnhancementModelKey = (settings as any).enhancementModel;
Comment on lines +41 to +42
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using as any to access legacy properties bypasses type safety. A safer alternative is to cast to Record<string, unknown> to indicate that you are accessing properties that may not be part of the defined type. This makes the intent clearer and is slightly safer than a full any cast.

Suggested change
const legacyValidationModelKey = (settings as any).validationModel;
const legacyEnhancementModelKey = (settings as any).enhancementModel;
const legacyValidationModelKey = (settings as Record<string, unknown>).validationModel;
const legacyEnhancementModelKey = (settings as Record<string, unknown>).enhancementModel;


const result = {
now: new Date().toISOString(),
defaults: {
DEFAULT_MODELS,
},
configured: {
defaultFeatureModelKey,
phaseModels: {
specGenerationModel: phaseModels.specGenerationModel,
backlogPlanningModel: phaseModels.backlogPlanningModel,
validationModel: phaseModels.validationModel,
},
legacy: {
validationModelKey: legacyValidationModelKey,
enhancementModelKey: legacyEnhancementModelKey,
},
},
resolved: {
defaultFeatureModel: {
key: defaultFeatureModelKey,
resolved: resolveModelString(defaultFeatureModelKey, DEFAULT_MODELS.claude),
},
phaseModels: {
specGenerationModel: specGeneration,
backlogPlanningModel: backlogPlanning,
validationModel: validation,
},
legacy: {
validationModel: {
key: legacyValidationModelKey,
resolved: resolveModelString(legacyValidationModelKey, DEFAULT_MODELS.claude),
},
enhancementModel: {
key: legacyEnhancementModelKey,
resolved: resolveModelString(legacyEnhancementModelKey, DEFAULT_MODELS.claude),
},
},
},
};

res.json(result);
});
Comment on lines +23 to +85
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Express 4 async route handlers error handling promise rejection

💡 Result:

In Express 4, an async route handler returns a Promise, but Express 4 does not observe/await the returned promise, so a thrown error or rejected promise won’t automatically reach your error middleware (often showing up as an unhandled rejection instead). [1]

Option A (Express 4): wrap and forward to next(err)

const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await findUser(req.params.id); // if rejects -> caught -> next(err)
  res.json(user);
}));

app.use((err, req, res, next) => {
  res.status(500).json({ error: err.message });
});

This pattern is widely used to ensure rejections get forwarded to Express’s error handler. [2]

Option B (Express 4): use express-async-errors

require('express-async-errors'); // once, before routes

This patches Express so errors thrown in async handlers are routed to your error middleware. [3]

Option C: upgrade to Express 5

Express 5 handles rejected promises from middleware/handlers by forwarding them to error-handling middleware (effectively like next(err)). [4]

Sources:
[1] Stack Overflow explanation of why Express 4 doesn’t catch async rejections: it doesn’t use the returned promise https://stackoverflow.com/questions/49417580/express-middleware-cannot-trap-errors-thrown-by-async-await-but-why
[2] Example wrapper pattern (Promise.resolve(...).catch(next)) https://betterstack.com/community/guides/scaling-nodejs/error-handling-express/
[3] express-async-errors usage/docs https://www.npmjs.com/package/express-async-errors
[4] Express 5 migration guide: rejected promises handled from middleware/handlers https://expressjs.com/ko/guide/migrating-5.html


🏁 Script executed:

cat -n apps/server/src/routes/debug/index.ts | head -100

Repository: AutoMaker-Org/automaker

Length of output: 3649


🏁 Script executed:

rg "express-async-errors" --type ts --type tsx --type js

Repository: AutoMaker-Org/automaker

Length of output: 94


🏁 Script executed:

rg "asyncHandler|async.*catch|Promise\.resolve.*catch" apps/server/src --type ts --type tsx -A 2

Repository: AutoMaker-Org/automaker

Length of output: 94


🏁 Script executed:

find apps/server/src/routes -name "*.ts" -o -name "*.tsx" | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && head -30 {}'

Repository: AutoMaker-Org/automaker

Length of output: 4580


Add error handling to async route handler.

Express 4 does not automatically catch rejected promises from async route handlers—if settingsService.getGlobalSettings() throws, the unhandled rejection will bypass error middleware. Wrap the handler body in try/catch to match the error handling pattern used throughout the codebase (see apps/server/src/routes/agent/routes/*.ts).

🛡️ Proposed fix
   router.get('/resolved-models', async (_req: Request, res: Response) => {
+    try {
       const settings = await settingsService.getGlobalSettings();
       // ... existing body ...
       res.json(result);
+    } catch (error) {
+      res.status(500).json({ error: 'Failed to resolve models' });
+    }
   });
🤖 Prompt for AI Agents
In `@apps/server/src/routes/debug/index.ts` around lines 23 - 85, The async route
handler registered with router.get('/resolved-models') must catch errors from
await settingsService.getGlobalSettings() (and other async calls); update the
handler signature to include next: NextFunction, wrap the existing body in
try/catch, and on error call next(err) so Express error middleware handles it.
Ensure you import NextFunction from express if needed and keep all existing
logic (resolvePhaseModel/resolveModelString and res.json(result)) inside the try
block.


return router;
}
2 changes: 1 addition & 1 deletion apps/server/src/routes/features/routes/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { createLogger } from '@automaker/utils';
const logger = createLogger('features/update');

// Statuses that should trigger syncing to app_spec.txt
const SYNC_TRIGGER_STATUSES: FeatureStatus[] = ['verified', 'completed'];
const SYNC_TRIGGER_STATUSES: FeatureStatus[] = ['verified', 'done', 'completed'];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Stale inline comment on Line 74.

The comment on Line 74 says "verified or completed" but SYNC_TRIGGER_STATUSES now also includes 'done'. Consider updating it to match.

📝 Suggested fix
-      // Trigger sync to app_spec.txt when status changes to verified or completed
+      // Trigger sync to app_spec.txt when status changes to verified, done, or completed
🤖 Prompt for AI Agents
In `@apps/server/src/routes/features/routes/update.ts` at line 14, Update the
stale inline comment that references "verified or completed" to reflect the
current SYNC_TRIGGER_STATUSES array which includes 'verified', 'done', and
'completed'; locate the comment near the SYNC_TRIGGER_STATUSES constant (symbol:
SYNC_TRIGGER_STATUSES) and change the wording to mention "verified, done, or
completed" (or an equivalent phrase) so the comment matches the actual values.


export function createUpdateHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise<void> => {
Expand Down
100 changes: 17 additions & 83 deletions apps/server/src/routes/worktree/routes/merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,8 @@
*/

import type { Request, Response } from 'express';
import { exec } from 'child_process';
import { promisify } from 'util';
import { getErrorMessage, logError, isValidBranchName, execGitCommand } from '../common.js';
import { createLogger } from '@automaker/utils';

const execAsync = promisify(exec);
const logger = createLogger('Worktree');
import { getErrorMessage, logError } from '../common.js';
import { mergeWorktreeBranch, cleanupWorktree } from '@automaker/git-utils';

export function createMergeHandler() {
return async (req: Request, res: Response): Promise<void> => {
Expand All @@ -38,102 +33,41 @@ export function createMergeHandler() {
// Determine the target branch (default to 'main')
const mergeTo = targetBranch || 'main';

// Validate source branch exists
try {
await execAsync(`git rev-parse --verify ${branchName}`, { cwd: projectPath });
} catch {
res.status(400).json({
success: false,
error: `Branch "${branchName}" does not exist`,
});
return;
}

// Validate target branch exists
try {
await execAsync(`git rev-parse --verify ${mergeTo}`, { cwd: projectPath });
} catch {
res.status(400).json({
success: false,
error: `Target branch "${mergeTo}" does not exist`,
});
return;
}

// Merge the feature branch into the target branch
const mergeCmd = options?.squash
? `git merge --squash ${branchName}`
: `git merge ${branchName} -m "${options?.message || `Merge ${branchName} into ${mergeTo}`}"`;

try {
await execAsync(mergeCmd, { cwd: projectPath });
} catch (mergeError: unknown) {
// Check if this is a merge conflict
const err = mergeError as { stdout?: string; stderr?: string; message?: string };
const output = `${err.stdout || ''} ${err.stderr || ''} ${err.message || ''}`;
const hasConflicts =
output.includes('CONFLICT') || output.includes('Automatic merge failed');
// Merge using shared utility
const result = await mergeWorktreeBranch(projectPath, branchName, mergeTo, {
squash: options?.squash,
message: options?.message,
});

if (hasConflicts) {
// Return conflict-specific error message that frontend can detect
if (!result.success) {
if (result.hasConflicts) {
res.status(409).json({
success: false,
error: `Merge CONFLICT: Automatic merge of "${branchName}" into "${mergeTo}" failed. Please resolve conflicts manually.`,
error: result.error,
hasConflicts: true,
});
return;
}

// Re-throw non-conflict errors to be handled by outer catch
throw mergeError;
}

// If squash merge, need to commit
if (options?.squash) {
await execAsync(`git commit -m "${options?.message || `Merge ${branchName} (squash)`}"`, {
cwd: projectPath,
res.status(400).json({
success: false,
error: result.error,
});
return;
}

// Optionally delete the worktree and branch after merging
let worktreeDeleted = false;
let branchDeleted = false;
let deleted: { worktreeDeleted: boolean; branchDeleted: boolean } | undefined;

if (options?.deleteWorktreeAndBranch) {
// Remove the worktree
try {
await execGitCommand(['worktree', 'remove', worktreePath, '--force'], projectPath);
worktreeDeleted = true;
} catch {
// Try with prune if remove fails
try {
await execGitCommand(['worktree', 'prune'], projectPath);
worktreeDeleted = true;
} catch {
logger.warn(`Failed to remove worktree: ${worktreePath}`);
}
}

// Delete the branch (but not main/master)
if (branchName !== 'main' && branchName !== 'master') {
if (!isValidBranchName(branchName)) {
logger.warn(`Invalid branch name detected, skipping deletion: ${branchName}`);
} else {
try {
await execGitCommand(['branch', '-D', branchName], projectPath);
branchDeleted = true;
} catch {
logger.warn(`Failed to delete branch: ${branchName}`);
}
}
}
deleted = await cleanupWorktree(projectPath, worktreePath, branchName);
}

res.json({
success: true,
mergedBranch: branchName,
targetBranch: mergeTo,
deleted: options?.deleteWorktreeAndBranch ? { worktreeDeleted, branchDeleted } : undefined,
deleted,
});
} catch (error) {
logError(error, 'Merge worktree failed');
Expand Down
Loading