mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-17 22:13:08 +00:00
Add quick-add feature with improved workflows (#802)
* Changes from feature/quick-add * feat: Clarify system prompt and improve error handling across services. Address PR Feedback * feat: Improve PR description parsing and refactor event handling * feat: Add context options to pipeline orchestrator initialization * fix: Deduplicate React and handle CJS interop for use-sync-external-store Resolve "Cannot read properties of null (reading 'useState')" errors by deduplicating React/react-dom and ensuring use-sync-external-store is bundled together with React to prevent CJS packages from resolving to different React instances.
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
import type {
|
||||
PlanningMode,
|
||||
ThinkingLevel,
|
||||
ReasoningEffort,
|
||||
ParsedTask,
|
||||
ClaudeCompatibleProvider,
|
||||
Credentials,
|
||||
@@ -24,7 +25,9 @@ export interface AgentExecutionOptions {
|
||||
previousContent?: string;
|
||||
systemPrompt?: string;
|
||||
autoLoadClaudeMd?: boolean;
|
||||
useClaudeCodeSystemPrompt?: boolean;
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
reasoningEffort?: ReasoningEffort;
|
||||
branchName?: string | null;
|
||||
credentials?: Credentials;
|
||||
claudeCompatibleProvider?: ClaudeCompatibleProvider;
|
||||
|
||||
@@ -128,6 +128,7 @@ export class AgentExecutor {
|
||||
? (mcpServers as Record<string, { command: string }>)
|
||||
: undefined,
|
||||
thinkingLevel: options.thinkingLevel,
|
||||
reasoningEffort: options.reasoningEffort,
|
||||
credentials,
|
||||
claudeCompatibleProvider,
|
||||
sdkSessionId,
|
||||
@@ -703,6 +704,7 @@ export class AgentExecutor {
|
||||
allowedTools: o.sdkOptions?.allowedTools as string[] | undefined,
|
||||
abortController: o.abortController,
|
||||
thinkingLevel: o.thinkingLevel,
|
||||
reasoningEffort: o.reasoningEffort,
|
||||
mcpServers:
|
||||
o.mcpServers && Object.keys(o.mcpServers).length > 0
|
||||
? (o.mcpServers as Record<string, { command: string }>)
|
||||
|
||||
@@ -21,6 +21,7 @@ import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.
|
||||
import type { SettingsService } from './settings-service.js';
|
||||
import {
|
||||
getAutoLoadClaudeMdSetting,
|
||||
getUseClaudeCodeSystemPromptSetting,
|
||||
filterClaudeMdFromContext,
|
||||
getMCPServersFromSettings,
|
||||
getPromptCustomization,
|
||||
@@ -357,6 +358,22 @@ export class AgentService {
|
||||
'[AgentService]'
|
||||
);
|
||||
|
||||
// Load useClaudeCodeSystemPrompt setting (project setting takes precedence over global)
|
||||
// Wrap in try/catch so transient settingsService errors don't abort message processing
|
||||
let useClaudeCodeSystemPrompt = true;
|
||||
try {
|
||||
useClaudeCodeSystemPrompt = await getUseClaudeCodeSystemPromptSetting(
|
||||
effectiveWorkDir,
|
||||
this.settingsService,
|
||||
'[AgentService]'
|
||||
);
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
'[AgentService] getUseClaudeCodeSystemPromptSetting failed, defaulting to true',
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
// Load MCP servers from settings (global setting only)
|
||||
const mcpServers = await getMCPServersFromSettings(this.settingsService, '[AgentService]');
|
||||
|
||||
@@ -443,6 +460,7 @@ export class AgentService {
|
||||
systemPrompt: combinedSystemPrompt,
|
||||
abortController: session.abortController!,
|
||||
autoLoadClaudeMd,
|
||||
useClaudeCodeSystemPrompt,
|
||||
thinkingLevel: effectiveThinkingLevel, // Pass thinking level for Claude models
|
||||
maxTurns: userMaxTurns, // User-configured max turns from settings
|
||||
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import path from 'path';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import type { Feature, PlanningMode, ThinkingLevel } from '@automaker/types';
|
||||
import type { Feature, PlanningMode, ThinkingLevel, ReasoningEffort } from '@automaker/types';
|
||||
import { DEFAULT_MAX_CONCURRENCY, DEFAULT_MODELS, stripProviderPrefix } from '@automaker/types';
|
||||
import { resolveModelString } from '@automaker/model-resolver';
|
||||
import { createLogger, loadContextFiles, classifyError } from '@automaker/utils';
|
||||
@@ -213,7 +213,9 @@ export class AutoModeServiceFacade {
|
||||
previousContent?: string;
|
||||
systemPrompt?: string;
|
||||
autoLoadClaudeMd?: boolean;
|
||||
useClaudeCodeSystemPrompt?: boolean;
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
reasoningEffort?: ReasoningEffort;
|
||||
branchName?: string | null;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -244,6 +246,7 @@ export class AutoModeServiceFacade {
|
||||
// internal defaults which may be much lower than intended (e.g., Codex CLI's
|
||||
// default turn limit can cause feature runs to stop prematurely).
|
||||
const autoLoadClaudeMd = opts?.autoLoadClaudeMd ?? false;
|
||||
const useClaudeCodeSystemPrompt = opts?.useClaudeCodeSystemPrompt ?? true;
|
||||
let mcpServers: Record<string, unknown> | undefined;
|
||||
try {
|
||||
if (settingsService) {
|
||||
@@ -265,6 +268,7 @@ export class AutoModeServiceFacade {
|
||||
systemPrompt: opts?.systemPrompt,
|
||||
abortController,
|
||||
autoLoadClaudeMd,
|
||||
useClaudeCodeSystemPrompt,
|
||||
thinkingLevel: opts?.thinkingLevel,
|
||||
maxTurns: userMaxTurns,
|
||||
mcpServers: mcpServers as
|
||||
@@ -292,7 +296,9 @@ export class AutoModeServiceFacade {
|
||||
previousContent: opts?.previousContent as string | undefined,
|
||||
systemPrompt: opts?.systemPrompt as string | undefined,
|
||||
autoLoadClaudeMd: opts?.autoLoadClaudeMd as boolean | undefined,
|
||||
useClaudeCodeSystemPrompt,
|
||||
thinkingLevel: opts?.thinkingLevel as ThinkingLevel | undefined,
|
||||
reasoningEffort: opts?.reasoningEffort as ReasoningEffort | undefined,
|
||||
branchName: opts?.branchName as string | null | undefined,
|
||||
provider,
|
||||
effectiveBareModel,
|
||||
|
||||
@@ -64,6 +64,8 @@ interface AutoModeEventPayload {
|
||||
error?: string;
|
||||
errorType?: string;
|
||||
projectPath?: string;
|
||||
/** Status field present when type === 'feature_status_changed' */
|
||||
status?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,6 +77,28 @@ interface FeatureCreatedPayload {
|
||||
projectPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Feature status changed event payload structure
|
||||
*/
|
||||
interface FeatureStatusChangedPayload {
|
||||
featureId: string;
|
||||
projectPath: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to safely narrow AutoModeEventPayload to FeatureStatusChangedPayload
|
||||
*/
|
||||
function isFeatureStatusChangedPayload(
|
||||
payload: AutoModeEventPayload
|
||||
): payload is AutoModeEventPayload & FeatureStatusChangedPayload {
|
||||
return (
|
||||
typeof payload.featureId === 'string' &&
|
||||
typeof payload.projectPath === 'string' &&
|
||||
typeof payload.status === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Event Hook Service
|
||||
*
|
||||
@@ -82,12 +106,30 @@ interface FeatureCreatedPayload {
|
||||
* Also stores events to history for debugging and replay.
|
||||
*/
|
||||
export class EventHookService {
|
||||
/** Feature status that indicates agent work is done and awaiting human review (tests skipped) */
|
||||
private static readonly STATUS_WAITING_APPROVAL = 'waiting_approval';
|
||||
/** Feature status that indicates agent work passed automated verification */
|
||||
private static readonly STATUS_VERIFIED = 'verified';
|
||||
|
||||
private emitter: EventEmitter | null = null;
|
||||
private settingsService: SettingsService | null = null;
|
||||
private eventHistoryService: EventHistoryService | null = null;
|
||||
private featureLoader: FeatureLoader | null = null;
|
||||
private unsubscribe: (() => void) | null = null;
|
||||
|
||||
/**
|
||||
* Track feature IDs that have already had hooks fired via auto_mode_feature_complete
|
||||
* to prevent double-firing when feature_status_changed also fires for the same feature.
|
||||
* Entries are automatically cleaned up after 30 seconds.
|
||||
*/
|
||||
private recentlyHandledFeatures = new Set<string>();
|
||||
|
||||
/**
|
||||
* Timer IDs for pending cleanup of recentlyHandledFeatures entries,
|
||||
* keyed by featureId. Stored so they can be cancelled in destroy().
|
||||
*/
|
||||
private recentlyHandledTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
/**
|
||||
* Initialize the service with event emitter, settings service, event history service, and feature loader
|
||||
*/
|
||||
@@ -122,6 +164,12 @@ export class EventHookService {
|
||||
this.unsubscribe();
|
||||
this.unsubscribe = null;
|
||||
}
|
||||
// Cancel all pending cleanup timers to avoid cross-session mutations
|
||||
for (const timerId of this.recentlyHandledTimers.values()) {
|
||||
clearTimeout(timerId);
|
||||
}
|
||||
this.recentlyHandledTimers.clear();
|
||||
this.recentlyHandledFeatures.clear();
|
||||
this.emitter = null;
|
||||
this.settingsService = null;
|
||||
this.eventHistoryService = null;
|
||||
@@ -140,14 +188,27 @@ export class EventHookService {
|
||||
switch (payload.type) {
|
||||
case 'auto_mode_feature_complete':
|
||||
trigger = payload.passes ? 'feature_success' : 'feature_error';
|
||||
// Track this feature so feature_status_changed doesn't double-fire hooks
|
||||
if (payload.featureId) {
|
||||
this.markFeatureHandled(payload.featureId);
|
||||
}
|
||||
break;
|
||||
case 'auto_mode_error':
|
||||
// Feature-level error (has featureId) vs auto-mode level error
|
||||
trigger = payload.featureId ? 'feature_error' : 'auto_mode_error';
|
||||
// Track this feature so feature_status_changed doesn't double-fire hooks
|
||||
if (payload.featureId) {
|
||||
this.markFeatureHandled(payload.featureId);
|
||||
}
|
||||
break;
|
||||
case 'auto_mode_idle':
|
||||
trigger = 'auto_mode_complete';
|
||||
break;
|
||||
case 'feature_status_changed':
|
||||
if (isFeatureStatusChangedPayload(payload)) {
|
||||
this.handleFeatureStatusChanged(payload);
|
||||
}
|
||||
return;
|
||||
default:
|
||||
// Other event types don't trigger hooks
|
||||
return;
|
||||
@@ -203,6 +264,74 @@ export class EventHookService {
|
||||
await this.executeHooksForTrigger('feature_created', context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle feature_status_changed events for non-auto-mode feature completion.
|
||||
*
|
||||
* Auto-mode features already emit auto_mode_feature_complete which triggers hooks.
|
||||
* This handler catches manual (non-auto-mode) feature completions by detecting
|
||||
* status transitions to completion states (verified, waiting_approval).
|
||||
*/
|
||||
private async handleFeatureStatusChanged(payload: FeatureStatusChangedPayload): Promise<void> {
|
||||
// Skip if this feature was already handled via auto_mode_feature_complete
|
||||
if (this.recentlyHandledFeatures.has(payload.featureId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let trigger: EventHookTrigger | null = null;
|
||||
|
||||
if (
|
||||
payload.status === EventHookService.STATUS_VERIFIED ||
|
||||
payload.status === EventHookService.STATUS_WAITING_APPROVAL
|
||||
) {
|
||||
trigger = 'feature_success';
|
||||
} else {
|
||||
// Only completion statuses trigger hooks from status changes
|
||||
return;
|
||||
}
|
||||
|
||||
// Load feature name
|
||||
let featureName: string | undefined = undefined;
|
||||
if (this.featureLoader) {
|
||||
try {
|
||||
const feature = await this.featureLoader.get(payload.projectPath, payload.featureId);
|
||||
if (feature?.title) {
|
||||
featureName = feature.title;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to load feature ${payload.featureId} for status change hook:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
const context: HookContext = {
|
||||
featureId: payload.featureId,
|
||||
featureName,
|
||||
projectPath: payload.projectPath,
|
||||
projectName: this.extractProjectName(payload.projectPath),
|
||||
timestamp: new Date().toISOString(),
|
||||
eventType: trigger,
|
||||
};
|
||||
|
||||
await this.executeHooksForTrigger(trigger, context, { passes: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a feature as recently handled to prevent double-firing hooks.
|
||||
* Entries are cleaned up after 30 seconds.
|
||||
*/
|
||||
private markFeatureHandled(featureId: string): void {
|
||||
// Cancel any existing timer for this feature before setting a new one
|
||||
const existing = this.recentlyHandledTimers.get(featureId);
|
||||
if (existing !== undefined) {
|
||||
clearTimeout(existing);
|
||||
}
|
||||
this.recentlyHandledFeatures.add(featureId);
|
||||
const timerId = setTimeout(() => {
|
||||
this.recentlyHandledFeatures.delete(featureId);
|
||||
this.recentlyHandledTimers.delete(featureId);
|
||||
}, 30000);
|
||||
this.recentlyHandledTimers.set(featureId, timerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute all enabled hooks matching the given trigger and store event to history
|
||||
*/
|
||||
|
||||
@@ -12,6 +12,7 @@ import * as secureFs from '../lib/secure-fs.js';
|
||||
import {
|
||||
getPromptCustomization,
|
||||
getAutoLoadClaudeMdSetting,
|
||||
getUseClaudeCodeSystemPromptSetting,
|
||||
filterClaudeMdFromContext,
|
||||
} from '../lib/settings-helpers.js';
|
||||
import { validateWorkingDirectory } from '../lib/sdk-options.js';
|
||||
@@ -241,6 +242,11 @@ ${feature.spec}
|
||||
this.settingsService,
|
||||
'[ExecutionService]'
|
||||
);
|
||||
const useClaudeCodeSystemPrompt = await getUseClaudeCodeSystemPromptSetting(
|
||||
projectPath,
|
||||
this.settingsService,
|
||||
'[ExecutionService]'
|
||||
);
|
||||
const prompts = await getPromptCustomization(this.settingsService, '[ExecutionService]');
|
||||
let prompt: string;
|
||||
const contextResult = await this.loadContextFilesFn({
|
||||
@@ -289,7 +295,9 @@ ${feature.spec}
|
||||
requirePlanApproval: feature.requirePlanApproval,
|
||||
systemPrompt: combinedSystemPrompt || undefined,
|
||||
autoLoadClaudeMd,
|
||||
useClaudeCodeSystemPrompt,
|
||||
thinkingLevel: feature.thinkingLevel,
|
||||
reasoningEffort: feature.reasoningEffort,
|
||||
branchName: feature.branchName ?? null,
|
||||
}
|
||||
);
|
||||
@@ -353,7 +361,9 @@ Please continue from where you left off and complete all remaining tasks. Use th
|
||||
requirePlanApproval: false,
|
||||
systemPrompt: combinedSystemPrompt || undefined,
|
||||
autoLoadClaudeMd,
|
||||
useClaudeCodeSystemPrompt,
|
||||
thinkingLevel: feature.thinkingLevel,
|
||||
reasoningEffort: feature.reasoningEffort,
|
||||
branchName: feature.branchName ?? null,
|
||||
}
|
||||
);
|
||||
@@ -388,6 +398,7 @@ Please continue from where you left off and complete all remaining tasks. Use th
|
||||
branchName: feature.branchName ?? null,
|
||||
abortController,
|
||||
autoLoadClaudeMd,
|
||||
useClaudeCodeSystemPrompt,
|
||||
testAttempts: 0,
|
||||
maxTestAttempts: 5,
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* allowing the service to delegate to other services without circular dependencies.
|
||||
*/
|
||||
|
||||
import type { Feature, PlanningMode, ThinkingLevel } from '@automaker/types';
|
||||
import type { Feature, PlanningMode, ThinkingLevel, ReasoningEffort } from '@automaker/types';
|
||||
import type { loadContextFiles } from '@automaker/utils';
|
||||
import type { PipelineContext } from './pipeline-orchestrator.js';
|
||||
|
||||
@@ -31,7 +31,9 @@ export type RunAgentFn = (
|
||||
previousContent?: string;
|
||||
systemPrompt?: string;
|
||||
autoLoadClaudeMd?: boolean;
|
||||
useClaudeCodeSystemPrompt?: boolean;
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
reasoningEffort?: ReasoningEffort;
|
||||
branchName?: string | null;
|
||||
}
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -16,6 +16,7 @@ import * as secureFs from '../lib/secure-fs.js';
|
||||
import {
|
||||
getPromptCustomization,
|
||||
getAutoLoadClaudeMdSetting,
|
||||
getUseClaudeCodeSystemPromptSetting,
|
||||
filterClaudeMdFromContext,
|
||||
} from '../lib/settings-helpers.js';
|
||||
import { validateWorkingDirectory } from '../lib/sdk-options.js';
|
||||
@@ -70,8 +71,16 @@ export class PipelineOrchestrator {
|
||||
) {}
|
||||
|
||||
async executePipeline(ctx: PipelineContext): Promise<void> {
|
||||
const { projectPath, featureId, feature, steps, workDir, abortController, autoLoadClaudeMd } =
|
||||
ctx;
|
||||
const {
|
||||
projectPath,
|
||||
featureId,
|
||||
feature,
|
||||
steps,
|
||||
workDir,
|
||||
abortController,
|
||||
autoLoadClaudeMd,
|
||||
useClaudeCodeSystemPrompt,
|
||||
} = ctx;
|
||||
const prompts = await getPromptCustomization(this.settingsService, '[AutoMode]');
|
||||
const contextResult = await this.loadContextFilesFn({
|
||||
projectPath,
|
||||
@@ -121,7 +130,9 @@ export class PipelineOrchestrator {
|
||||
previousContent: previousContext,
|
||||
systemPrompt: contextFilesPrompt || undefined,
|
||||
autoLoadClaudeMd,
|
||||
useClaudeCodeSystemPrompt,
|
||||
thinkingLevel: feature.thinkingLevel,
|
||||
reasoningEffort: feature.reasoningEffort,
|
||||
}
|
||||
);
|
||||
try {
|
||||
@@ -354,6 +365,11 @@ export class PipelineOrchestrator {
|
||||
this.settingsService,
|
||||
'[AutoMode]'
|
||||
);
|
||||
const useClaudeCodeSystemPrompt = await getUseClaudeCodeSystemPromptSetting(
|
||||
projectPath,
|
||||
this.settingsService,
|
||||
'[AutoMode]'
|
||||
);
|
||||
const context: PipelineContext = {
|
||||
projectPath,
|
||||
featureId,
|
||||
@@ -364,6 +380,7 @@ export class PipelineOrchestrator {
|
||||
branchName: branchName ?? null,
|
||||
abortController,
|
||||
autoLoadClaudeMd,
|
||||
useClaudeCodeSystemPrompt,
|
||||
testAttempts: 0,
|
||||
maxTestAttempts: 5,
|
||||
};
|
||||
@@ -462,7 +479,14 @@ export class PipelineOrchestrator {
|
||||
projectPath,
|
||||
undefined,
|
||||
undefined,
|
||||
{ projectPath, planningMode: 'skip', requirePlanApproval: false }
|
||||
{
|
||||
projectPath,
|
||||
planningMode: 'skip',
|
||||
requirePlanApproval: false,
|
||||
useClaudeCodeSystemPrompt: context.useClaudeCodeSystemPrompt,
|
||||
autoLoadClaudeMd: context.autoLoadClaudeMd,
|
||||
reasoningEffort: context.feature.reasoningEffort,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -596,7 +620,7 @@ export class PipelineOrchestrator {
|
||||
}
|
||||
// Only capture assertion details when they appear in failure context
|
||||
// or match explicit assertion error / expect patterns
|
||||
if (trimmed.includes('AssertionError') || trimmed.includes('AssertionError')) {
|
||||
if (trimmed.includes('AssertionError')) {
|
||||
failedTests.push(trimmed);
|
||||
} else if (
|
||||
inFailureContext &&
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface PipelineContext {
|
||||
branchName: string | null;
|
||||
abortController: AbortController;
|
||||
autoLoadClaudeMd: boolean;
|
||||
useClaudeCodeSystemPrompt?: boolean;
|
||||
testAttempts: number;
|
||||
maxTestAttempts: number;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import type {
|
||||
WorktreeInfo,
|
||||
PhaseModelConfig,
|
||||
PhaseModelEntry,
|
||||
FeatureTemplate,
|
||||
ClaudeApiProfile,
|
||||
ClaudeCompatibleProvider,
|
||||
ProviderModel,
|
||||
@@ -40,6 +41,7 @@ import {
|
||||
DEFAULT_CREDENTIALS,
|
||||
DEFAULT_PROJECT_SETTINGS,
|
||||
DEFAULT_PHASE_MODELS,
|
||||
DEFAULT_FEATURE_TEMPLATES,
|
||||
SETTINGS_VERSION,
|
||||
CREDENTIALS_VERSION,
|
||||
PROJECT_SETTINGS_VERSION,
|
||||
@@ -139,6 +141,11 @@ export class SettingsService {
|
||||
// Migrate model IDs to canonical format
|
||||
const migratedModelSettings = this.migrateModelSettings(settings);
|
||||
|
||||
// Merge built-in feature templates: ensure all built-in templates exist in user settings.
|
||||
// User customizations (enabled/disabled state, order overrides) are preserved.
|
||||
// New built-in templates added in code updates are injected for existing users.
|
||||
const mergedFeatureTemplates = this.mergeBuiltInTemplates(settings.featureTemplates);
|
||||
|
||||
// Apply any missing defaults (for backwards compatibility)
|
||||
let result: GlobalSettings = {
|
||||
...DEFAULT_GLOBAL_SETTINGS,
|
||||
@@ -149,6 +156,7 @@ export class SettingsService {
|
||||
...settings.keyboardShortcuts,
|
||||
},
|
||||
phaseModels: migratedPhaseModels,
|
||||
featureTemplates: mergedFeatureTemplates,
|
||||
};
|
||||
|
||||
// Version-based migrations
|
||||
@@ -250,6 +258,32 @@ export class SettingsService {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge built-in feature templates with user's stored templates.
|
||||
*
|
||||
* Ensures new built-in templates added in code updates are available to existing users
|
||||
* without overwriting their customizations (e.g., enabled/disabled state, custom order).
|
||||
* Built-in templates missing from stored settings are appended with their defaults.
|
||||
*
|
||||
* @param storedTemplates - Templates from user's settings file (may be undefined for new installs)
|
||||
* @returns Merged template list with all built-in templates present
|
||||
*/
|
||||
private mergeBuiltInTemplates(storedTemplates: FeatureTemplate[] | undefined): FeatureTemplate[] {
|
||||
if (!storedTemplates) {
|
||||
return DEFAULT_FEATURE_TEMPLATES;
|
||||
}
|
||||
|
||||
const storedIds = new Set(storedTemplates.map((t) => t.id));
|
||||
const missingBuiltIns = DEFAULT_FEATURE_TEMPLATES.filter((t) => !storedIds.has(t.id));
|
||||
|
||||
if (missingBuiltIns.length === 0) {
|
||||
return storedTemplates;
|
||||
}
|
||||
|
||||
// Append missing built-in templates after existing ones
|
||||
return [...storedTemplates, ...missingBuiltIns];
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate legacy enhancementModel/validationModel fields to phaseModels structure
|
||||
*
|
||||
|
||||
@@ -8,9 +8,64 @@
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
import { execFile } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import type { EventEmitter } from '../lib/events.js';
|
||||
import type { SettingsService } from './settings-service.js';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/**
|
||||
* Get the list of remote names that have a branch matching the given branch name.
|
||||
*
|
||||
* Uses `git for-each-ref` to check cached remote refs, returning the names of
|
||||
* any remotes that already have a branch with the same name as `currentBranch`.
|
||||
* Returns an empty array when `hasAnyRemotes` is false or when no matching
|
||||
* remote refs are found.
|
||||
*
|
||||
* This helps the UI distinguish between "branch exists on the tracking remote"
|
||||
* vs "branch was pushed to a different remote".
|
||||
*
|
||||
* @param worktreePath - Path to the git worktree
|
||||
* @param currentBranch - Branch name to search for on remotes
|
||||
* @param hasAnyRemotes - Whether the repository has any remotes configured
|
||||
* @returns Array of remote names (e.g. ["origin", "upstream"]) that contain the branch
|
||||
*/
|
||||
export async function getRemotesWithBranch(
|
||||
worktreePath: string,
|
||||
currentBranch: string,
|
||||
hasAnyRemotes: boolean
|
||||
): Promise<string[]> {
|
||||
if (!hasAnyRemotes) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout: remoteRefsOutput } = await execFileAsync(
|
||||
'git',
|
||||
['for-each-ref', '--format=%(refname:short)', `refs/remotes/*/${currentBranch}`],
|
||||
{ cwd: worktreePath }
|
||||
);
|
||||
|
||||
if (!remoteRefsOutput.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return remoteRefsOutput
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map((ref) => {
|
||||
// Extract remote name from "remote/branch" format
|
||||
const slashIdx = ref.indexOf('/');
|
||||
return slashIdx !== -1 ? ref.slice(0, slashIdx) : ref;
|
||||
})
|
||||
.filter((name) => name.length > 0);
|
||||
} catch {
|
||||
// Ignore errors - return empty array
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when one or more file copy operations fail during
|
||||
* `copyConfiguredFiles`. The caller can inspect `failures` for details.
|
||||
|
||||
Reference in New Issue
Block a user