mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
fix: adress pr comments
This commit is contained in:
@@ -274,49 +274,60 @@ function detectSpecFallback(text: string): boolean {
|
||||
* 4. **Problem**: or **Problem Statement**: section (spec/full modes)
|
||||
* 5. **Solution**: section as fallback
|
||||
*
|
||||
* Note: Uses last match for each pattern to avoid stale summaries
|
||||
* when agent output accumulates across multiple runs.
|
||||
*
|
||||
* @param text - The text content to extract summary from
|
||||
* @returns The extracted summary string, or null if no summary found
|
||||
*/
|
||||
function extractSummary(text: string): string | null {
|
||||
// Check for explicit <summary> tags first
|
||||
const summaryMatch = text.match(/<summary>([\s\S]*?)<\/summary>/);
|
||||
// Helper to truncate content to first paragraph with max length
|
||||
const truncate = (content: string, maxLength: number): string => {
|
||||
const firstPara = content.split(/\n\n/)[0];
|
||||
return firstPara.length > maxLength ? `${firstPara.substring(0, maxLength)}...` : firstPara;
|
||||
};
|
||||
|
||||
// Helper to get last match from matchAll results
|
||||
const getLastMatch = (matches: IterableIterator<RegExpMatchArray>): RegExpMatchArray | null => {
|
||||
const arr = [...matches];
|
||||
return arr.length > 0 ? arr[arr.length - 1] : null;
|
||||
};
|
||||
|
||||
// Check for explicit <summary> tags first (use last match to avoid stale summaries)
|
||||
const summaryMatches = text.matchAll(/<summary>([\s\S]*?)<\/summary>/g);
|
||||
const summaryMatch = getLastMatch(summaryMatches);
|
||||
if (summaryMatch) {
|
||||
return summaryMatch[1].trim();
|
||||
}
|
||||
|
||||
// Check for ## Summary section
|
||||
const sectionMatch = text.match(/##\s*Summary\s*\n+([\s\S]*?)(?=\n##|\n\*\*|$)/i);
|
||||
// Check for ## Summary section (use last match)
|
||||
const sectionMatches = text.matchAll(/##\s*Summary\s*\n+([\s\S]*?)(?=\n##|\n\*\*|$)/gi);
|
||||
const sectionMatch = getLastMatch(sectionMatches);
|
||||
if (sectionMatch) {
|
||||
const content = sectionMatch[1].trim();
|
||||
// Take first paragraph or up to 500 chars
|
||||
const firstPara = content.split(/\n\n/)[0];
|
||||
return firstPara.length > 500 ? firstPara.substring(0, 500) + '...' : firstPara;
|
||||
return truncate(sectionMatch[1].trim(), 500);
|
||||
}
|
||||
|
||||
// Check for **Goal**: section (lite mode)
|
||||
const goalMatch = text.match(/\*\*Goal\*\*:\s*(.+?)(?:\n|$)/i);
|
||||
// Check for **Goal**: section (lite mode, use last match)
|
||||
const goalMatches = text.matchAll(/\*\*Goal\*\*:\s*(.+?)(?:\n|$)/gi);
|
||||
const goalMatch = getLastMatch(goalMatches);
|
||||
if (goalMatch) {
|
||||
return goalMatch[1].trim();
|
||||
}
|
||||
|
||||
// Check for **Problem**: or **Problem Statement**: section (spec/full modes)
|
||||
const problemMatch = text.match(
|
||||
/\*\*Problem(?:\s*Statement)?\*\*:\s*([\s\S]*?)(?=\n\d+\.|\n\*\*|$)/i
|
||||
// Check for **Problem**: or **Problem Statement**: section (spec/full modes, use last match)
|
||||
const problemMatches = text.matchAll(
|
||||
/\*\*Problem(?:\s*Statement)?\*\*:\s*([\s\S]*?)(?=\n\d+\.|\n\*\*|$)/gi
|
||||
);
|
||||
const problemMatch = getLastMatch(problemMatches);
|
||||
if (problemMatch) {
|
||||
const content = problemMatch[1].trim();
|
||||
// Take first paragraph or up to 500 chars
|
||||
const firstPara = content.split(/\n\n/)[0];
|
||||
return firstPara.length > 500 ? firstPara.substring(0, 500) + '...' : firstPara;
|
||||
return truncate(problemMatch[1].trim(), 500);
|
||||
}
|
||||
|
||||
// Check for **Solution**: section as fallback
|
||||
const solutionMatch = text.match(/\*\*Solution\*\*:\s*([\s\S]*?)(?=\n\d+\.|\n\*\*|$)/i);
|
||||
// Check for **Solution**: section as fallback (use last match)
|
||||
const solutionMatches = text.matchAll(/\*\*Solution\*\*:\s*([\s\S]*?)(?=\n\d+\.|\n\*\*|$)/gi);
|
||||
const solutionMatch = getLastMatch(solutionMatches);
|
||||
if (solutionMatch) {
|
||||
const content = solutionMatch[1].trim();
|
||||
// Take first paragraph or up to 300 chars
|
||||
const firstPara = content.split(/\n\n/)[0];
|
||||
return firstPara.length > 300 ? firstPara.substring(0, 300) + '...' : firstPara;
|
||||
return truncate(solutionMatch[1].trim(), 300);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -4008,6 +4019,168 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
|
||||
);
|
||||
}, STREAM_HEARTBEAT_MS);
|
||||
|
||||
// RECOVERY PATH: If we have an approved plan with persisted tasks, skip spec generation
|
||||
// and directly execute the remaining tasks
|
||||
if (existingApprovedPlan && persistedTasks && persistedTasks.length > 0) {
|
||||
logger.info(
|
||||
`Recovery: Resuming task execution for feature ${featureId} with ${persistedTasks.length} tasks`
|
||||
);
|
||||
|
||||
// Get customized prompts for task execution
|
||||
const taskPrompts = await getPromptCustomization(this.settingsService, '[AutoMode]');
|
||||
const approvedPlanContent = existingApprovedPlan.content || '';
|
||||
|
||||
// Execute each task with a separate agent
|
||||
for (let taskIndex = 0; taskIndex < persistedTasks.length; taskIndex++) {
|
||||
const task = persistedTasks[taskIndex];
|
||||
|
||||
// Skip tasks that are already completed
|
||||
if (task.status === 'completed') {
|
||||
logger.info(`Skipping already completed task ${task.id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for abort
|
||||
if (abortController.signal.aborted) {
|
||||
throw new Error('Feature execution aborted');
|
||||
}
|
||||
|
||||
// Mark task as in_progress immediately (even without TASK_START marker)
|
||||
await this.updateTaskStatus(projectPath, featureId, task.id, 'in_progress');
|
||||
|
||||
// Emit task started
|
||||
logger.info(`Starting task ${task.id}: ${task.description}`);
|
||||
this.emitAutoModeEvent('auto_mode_task_started', {
|
||||
featureId,
|
||||
projectPath,
|
||||
branchName,
|
||||
taskId: task.id,
|
||||
taskDescription: task.description,
|
||||
taskIndex,
|
||||
tasksTotal: persistedTasks.length,
|
||||
});
|
||||
|
||||
// Update planSpec with current task
|
||||
await this.updateFeaturePlanSpec(projectPath, featureId, {
|
||||
currentTaskId: task.id,
|
||||
});
|
||||
|
||||
// Build focused prompt for this specific task
|
||||
const taskPrompt = this.buildTaskPrompt(
|
||||
task,
|
||||
persistedTasks,
|
||||
taskIndex,
|
||||
approvedPlanContent,
|
||||
taskPrompts.taskExecution.taskPromptTemplate,
|
||||
undefined
|
||||
);
|
||||
|
||||
// Execute task with dedicated agent
|
||||
const taskStream = provider.executeQuery({
|
||||
prompt: taskPrompt,
|
||||
model: bareModel,
|
||||
maxTurns: Math.min(maxTurns || 100, 50),
|
||||
cwd: workDir,
|
||||
allowedTools: allowedTools,
|
||||
abortController,
|
||||
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined,
|
||||
credentials,
|
||||
claudeCompatibleProvider,
|
||||
});
|
||||
|
||||
let taskOutput = '';
|
||||
let taskCompleteDetected = false;
|
||||
|
||||
// Process task stream
|
||||
for await (const msg of taskStream) {
|
||||
if (msg.type === 'assistant' && msg.message?.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === 'text') {
|
||||
const text = block.text || '';
|
||||
taskOutput += text;
|
||||
responseText += text;
|
||||
this.emitAutoModeEvent('auto_mode_progress', {
|
||||
featureId,
|
||||
branchName,
|
||||
content: text,
|
||||
});
|
||||
scheduleWrite();
|
||||
|
||||
// Detect [TASK_COMPLETE] marker
|
||||
if (!taskCompleteDetected) {
|
||||
const completeTaskId = detectTaskCompleteMarker(taskOutput);
|
||||
if (completeTaskId) {
|
||||
taskCompleteDetected = true;
|
||||
logger.info(`[TASK_COMPLETE] detected for ${completeTaskId}`);
|
||||
await this.updateTaskStatus(
|
||||
projectPath,
|
||||
featureId,
|
||||
completeTaskId,
|
||||
'completed'
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (block.type === 'tool_use') {
|
||||
this.emitAutoModeEvent('auto_mode_tool', {
|
||||
featureId,
|
||||
branchName,
|
||||
tool: block.name,
|
||||
input: block.input,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (msg.type === 'error') {
|
||||
throw new Error(msg.error || `Error during task ${task.id}`);
|
||||
} else if (msg.type === 'result' && msg.subtype === 'success') {
|
||||
taskOutput += msg.result || '';
|
||||
responseText += msg.result || '';
|
||||
}
|
||||
}
|
||||
|
||||
// If no [TASK_COMPLETE] marker was detected, still mark as completed
|
||||
if (!taskCompleteDetected) {
|
||||
await this.updateTaskStatus(projectPath, featureId, task.id, 'completed');
|
||||
}
|
||||
|
||||
// Emit task completed
|
||||
logger.info(`Task ${task.id} completed for feature ${featureId}`);
|
||||
this.emitAutoModeEvent('auto_mode_task_complete', {
|
||||
featureId,
|
||||
projectPath,
|
||||
branchName,
|
||||
taskId: task.id,
|
||||
tasksCompleted: taskIndex + 1,
|
||||
tasksTotal: persistedTasks.length,
|
||||
});
|
||||
|
||||
// Update planSpec with progress
|
||||
await this.updateFeaturePlanSpec(projectPath, featureId, {
|
||||
tasksCompleted: taskIndex + 1,
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(`Recovery: All tasks completed for feature ${featureId}`);
|
||||
|
||||
// Extract and save final summary
|
||||
const summary = extractSummary(responseText);
|
||||
if (summary) {
|
||||
await this.saveFeatureSummary(projectPath, featureId, summary);
|
||||
this.emitAutoModeEvent('auto_mode_summary', {
|
||||
featureId,
|
||||
projectPath,
|
||||
summary,
|
||||
});
|
||||
}
|
||||
|
||||
// Final write and cleanup
|
||||
clearInterval(streamHeartbeat);
|
||||
if (writeTimeout) {
|
||||
clearTimeout(writeTimeout);
|
||||
}
|
||||
await writeToFile();
|
||||
return;
|
||||
}
|
||||
|
||||
// Wrap stream processing in try/finally to ensure timeout cleanup on any error/abort
|
||||
try {
|
||||
streamLoop: for await (const msg of stream) {
|
||||
@@ -4359,6 +4532,9 @@ After generating the revised spec, output:
|
||||
throw new Error('Feature execution aborted');
|
||||
}
|
||||
|
||||
// Mark task as in_progress immediately (even without TASK_START marker)
|
||||
await this.updateTaskStatus(projectPath, featureId, task.id, 'in_progress');
|
||||
|
||||
// Emit task started
|
||||
logger.info(`Starting task ${task.id}: ${task.description}`);
|
||||
this.emitAutoModeEvent('auto_mode_task_started', {
|
||||
|
||||
@@ -179,9 +179,6 @@ export function AddFeatureDialog({
|
||||
// Model selection state
|
||||
const [modelEntry, setModelEntry] = useState<PhaseModelEntry>({ model: 'claude-opus' });
|
||||
|
||||
// All models support planning mode via marker-based instructions in prompts
|
||||
const modelSupportsPlanningMode = true;
|
||||
|
||||
// Planning mode state
|
||||
const [planningMode, setPlanningMode] = useState<PlanningMode>('skip');
|
||||
const [requirePlanApproval, setRequirePlanApproval] = useState(false);
|
||||
@@ -562,41 +559,13 @@ export function AddFeatureDialog({
|
||||
|
||||
<div className="grid gap-3 grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label
|
||||
className={cn(
|
||||
'text-xs text-muted-foreground',
|
||||
!modelSupportsPlanningMode && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
Planning
|
||||
</Label>
|
||||
{modelSupportsPlanningMode ? (
|
||||
<PlanningModeSelect
|
||||
mode={planningMode}
|
||||
onModeChange={setPlanningMode}
|
||||
testIdPrefix="add-feature-planning"
|
||||
compact
|
||||
/>
|
||||
) : (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<PlanningModeSelect
|
||||
mode="skip"
|
||||
onModeChange={() => {}}
|
||||
testIdPrefix="add-feature-planning"
|
||||
compact
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Planning modes are only available for Claude Provider</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<Label className="text-xs text-muted-foreground">Planning</Label>
|
||||
<PlanningModeSelect
|
||||
mode={planningMode}
|
||||
onModeChange={setPlanningMode}
|
||||
testIdPrefix="add-feature-planning"
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">Options</Label>
|
||||
@@ -620,20 +589,14 @@ export function AddFeatureDialog({
|
||||
id="add-feature-require-approval"
|
||||
checked={requirePlanApproval}
|
||||
onCheckedChange={(checked) => setRequirePlanApproval(!!checked)}
|
||||
disabled={
|
||||
!modelSupportsPlanningMode ||
|
||||
planningMode === 'skip' ||
|
||||
planningMode === 'lite'
|
||||
}
|
||||
disabled={planningMode === 'skip' || planningMode === 'lite'}
|
||||
data-testid="add-feature-require-approval-checkbox"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="add-feature-require-approval"
|
||||
className={cn(
|
||||
'text-xs font-normal',
|
||||
!modelSupportsPlanningMode ||
|
||||
planningMode === 'skip' ||
|
||||
planningMode === 'lite'
|
||||
planningMode === 'skip' || planningMode === 'lite'
|
||||
? 'cursor-not-allowed text-muted-foreground'
|
||||
: 'cursor-pointer'
|
||||
)}
|
||||
|
||||
@@ -119,9 +119,6 @@ export function EditFeatureDialog({
|
||||
reasoningEffort: feature?.reasoningEffort || 'none',
|
||||
}));
|
||||
|
||||
// All models support planning mode via marker-based instructions in prompts
|
||||
const modelSupportsPlanningMode = true;
|
||||
|
||||
// Track the source of description changes for history
|
||||
const [descriptionChangeSource, setDescriptionChangeSource] = useState<
|
||||
{ source: 'enhance'; mode: EnhancementMode } | 'edit' | null
|
||||
@@ -454,41 +451,13 @@ export function EditFeatureDialog({
|
||||
|
||||
<div className="grid gap-3 grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label
|
||||
className={cn(
|
||||
'text-xs text-muted-foreground',
|
||||
!modelSupportsPlanningMode && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
Planning
|
||||
</Label>
|
||||
{modelSupportsPlanningMode ? (
|
||||
<PlanningModeSelect
|
||||
mode={planningMode}
|
||||
onModeChange={setPlanningMode}
|
||||
testIdPrefix="edit-feature-planning"
|
||||
compact
|
||||
/>
|
||||
) : (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<PlanningModeSelect
|
||||
mode="skip"
|
||||
onModeChange={() => {}}
|
||||
testIdPrefix="edit-feature-planning"
|
||||
compact
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Planning modes are only available for Claude Provider</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<Label className="text-xs text-muted-foreground">Planning</Label>
|
||||
<PlanningModeSelect
|
||||
mode={planningMode}
|
||||
onModeChange={setPlanningMode}
|
||||
testIdPrefix="edit-feature-planning"
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">Options</Label>
|
||||
@@ -514,20 +483,14 @@ export function EditFeatureDialog({
|
||||
id="edit-feature-require-approval"
|
||||
checked={requirePlanApproval}
|
||||
onCheckedChange={(checked) => setRequirePlanApproval(!!checked)}
|
||||
disabled={
|
||||
!modelSupportsPlanningMode ||
|
||||
planningMode === 'skip' ||
|
||||
planningMode === 'lite'
|
||||
}
|
||||
disabled={planningMode === 'skip' || planningMode === 'lite'}
|
||||
data-testid="edit-feature-require-approval-checkbox"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="edit-feature-require-approval"
|
||||
className={cn(
|
||||
'text-xs font-normal',
|
||||
!modelSupportsPlanningMode ||
|
||||
planningMode === 'skip' ||
|
||||
planningMode === 'lite'
|
||||
planningMode === 'skip' || planningMode === 'lite'
|
||||
? 'cursor-not-allowed text-muted-foreground'
|
||||
: 'cursor-pointer'
|
||||
)}
|
||||
|
||||
@@ -24,7 +24,6 @@ import type { WorkMode } from '../shared';
|
||||
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
||||
import { isCursorModel, type PhaseModelEntry } from '@automaker/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
|
||||
interface MassEditDialogProps {
|
||||
open: boolean;
|
||||
@@ -236,8 +235,6 @@ export function MassEditDialog({
|
||||
const hasAnyApply = Object.values(applyState).some(Boolean);
|
||||
const isCurrentModelCursor = isCursorModel(model);
|
||||
const modelAllowsThinking = !isCurrentModelCursor && modelSupportsThinking(model);
|
||||
// All models support planning mode via marker-based instructions in prompts
|
||||
const modelSupportsPlanningMode = true;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(open) => !open && onClose()}>
|
||||
@@ -277,64 +274,30 @@ export function MassEditDialog({
|
||||
<div className="border-t border-border" />
|
||||
|
||||
{/* Planning Mode */}
|
||||
{modelSupportsPlanningMode ? (
|
||||
<FieldWrapper
|
||||
label="Planning Mode"
|
||||
isMixed={mixedValues.planningMode || mixedValues.requirePlanApproval}
|
||||
willApply={applyState.planningMode || applyState.requirePlanApproval}
|
||||
onApplyChange={(apply) =>
|
||||
setApplyState((prev) => ({
|
||||
...prev,
|
||||
planningMode: apply,
|
||||
requirePlanApproval: apply,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<PlanningModeSelect
|
||||
mode={planningMode}
|
||||
onModeChange={(newMode) => {
|
||||
setPlanningMode(newMode);
|
||||
// Auto-suggest approval based on mode, but user can override
|
||||
setRequirePlanApproval(newMode === 'spec' || newMode === 'full');
|
||||
}}
|
||||
requireApproval={requirePlanApproval}
|
||||
onRequireApprovalChange={setRequirePlanApproval}
|
||||
testIdPrefix="mass-edit-planning"
|
||||
/>
|
||||
</FieldWrapper>
|
||||
) : (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
'p-3 rounded-lg border transition-colors border-border bg-muted/20 opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox checked={false} disabled className="opacity-50" />
|
||||
<Label className="text-sm font-medium text-muted-foreground">
|
||||
Planning Mode
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="opacity-50 pointer-events-none">
|
||||
<PlanningModeSelect
|
||||
mode="skip"
|
||||
onModeChange={() => {}}
|
||||
testIdPrefix="mass-edit-planning"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Planning modes are only available for Claude Provider</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<FieldWrapper
|
||||
label="Planning Mode"
|
||||
isMixed={mixedValues.planningMode || mixedValues.requirePlanApproval}
|
||||
willApply={applyState.planningMode || applyState.requirePlanApproval}
|
||||
onApplyChange={(apply) =>
|
||||
setApplyState((prev) => ({
|
||||
...prev,
|
||||
planningMode: apply,
|
||||
requirePlanApproval: apply,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<PlanningModeSelect
|
||||
mode={planningMode}
|
||||
onModeChange={(newMode) => {
|
||||
setPlanningMode(newMode);
|
||||
// Auto-suggest approval based on mode, but user can override
|
||||
setRequirePlanApproval(newMode === 'spec' || newMode === 'full');
|
||||
}}
|
||||
requireApproval={requirePlanApproval}
|
||||
onRequireApprovalChange={setRequirePlanApproval}
|
||||
testIdPrefix="mass-edit-planning"
|
||||
/>
|
||||
</FieldWrapper>
|
||||
|
||||
{/* Priority */}
|
||||
<FieldWrapper
|
||||
|
||||
11
apps/ui/src/types/electron.d.ts
vendored
11
apps/ui/src/types/electron.d.ts
vendored
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import type { ClaudeUsageResponse, CodexUsageResponse } from '@/store/app-store';
|
||||
import type { ParsedTask } from '@automaker/types';
|
||||
|
||||
export interface ImageAttachment {
|
||||
id?: string; // Optional - may not be present in messages loaded from server
|
||||
@@ -339,14 +340,8 @@ export type AutoModeEvent =
|
||||
featureId: string;
|
||||
projectPath?: string;
|
||||
taskId: string;
|
||||
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||
tasks: Array<{
|
||||
id: string;
|
||||
description: string;
|
||||
filePath?: string;
|
||||
phase?: string;
|
||||
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||
}>;
|
||||
status: ParsedTask['status'];
|
||||
tasks: ParsedTask[];
|
||||
}
|
||||
| {
|
||||
type: 'auto_mode_summary';
|
||||
|
||||
Reference in New Issue
Block a user