mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
feat: cleanup autopilot commands and fix workflow state manager
This commit is contained in:
@@ -3,16 +3,8 @@
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import { WorkflowOrchestrator } from '@tm/core';
|
||||
import {
|
||||
AutopilotBaseOptions,
|
||||
hasWorkflowState,
|
||||
loadWorkflowState,
|
||||
createGitAdapter,
|
||||
createCommitMessageGenerator,
|
||||
OutputFormatter,
|
||||
saveWorkflowState
|
||||
} from './shared.js';
|
||||
import { WorkflowService, GitAdapter, CommitMessageGenerator } from '@tm/core';
|
||||
import { AutopilotBaseOptions, OutputFormatter } from './shared.js';
|
||||
|
||||
type CommitOptions = AutopilotBaseOptions;
|
||||
|
||||
@@ -43,47 +35,41 @@ export class CommitCommand extends Command {
|
||||
const formatter = new OutputFormatter(mergedOptions.json || false);
|
||||
|
||||
try {
|
||||
// Check for workflow state
|
||||
const hasState = await hasWorkflowState(mergedOptions.projectRoot!);
|
||||
if (!hasState) {
|
||||
const projectRoot = mergedOptions.projectRoot!;
|
||||
|
||||
// Create workflow service (manages WorkflowStateManager internally)
|
||||
const workflowService = new WorkflowService(projectRoot);
|
||||
|
||||
// Check if workflow exists
|
||||
if (!(await workflowService.hasWorkflow())) {
|
||||
formatter.error('No active workflow', {
|
||||
suggestion: 'Start a workflow with: autopilot start <taskId>'
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Load state
|
||||
const state = await loadWorkflowState(mergedOptions.projectRoot!);
|
||||
if (!state) {
|
||||
formatter.error('Failed to load workflow state');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const orchestrator = new WorkflowOrchestrator(state.context);
|
||||
orchestrator.restoreState(state);
|
||||
orchestrator.enableAutoPersist(async (newState) => {
|
||||
await saveWorkflowState(mergedOptions.projectRoot!, newState);
|
||||
});
|
||||
// Resume workflow (loads state with single WorkflowStateManager instance)
|
||||
await workflowService.resumeWorkflow();
|
||||
const status = workflowService.getStatus();
|
||||
const workflowContext = workflowService.getContext();
|
||||
|
||||
// Verify in COMMIT phase
|
||||
const tddPhase = orchestrator.getCurrentTDDPhase();
|
||||
if (tddPhase !== 'COMMIT') {
|
||||
if (status.tddPhase !== 'COMMIT') {
|
||||
formatter.error('Not in COMMIT phase', {
|
||||
currentPhase: tddPhase || orchestrator.getCurrentPhase(),
|
||||
currentPhase: status.tddPhase || status.phase,
|
||||
suggestion: 'Complete RED and GREEN phases first'
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Get current subtask
|
||||
const currentSubtask = orchestrator.getCurrentSubtask();
|
||||
if (!currentSubtask) {
|
||||
// Verify there's an active subtask
|
||||
if (!status.currentSubtask) {
|
||||
formatter.error('No current subtask');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Initialize git adapter
|
||||
const gitAdapter = createGitAdapter(mergedOptions.projectRoot!);
|
||||
const gitAdapter = new GitAdapter(projectRoot);
|
||||
await gitAdapter.ensureGitRepository();
|
||||
|
||||
// Check for staged changes
|
||||
@@ -95,20 +81,20 @@ export class CommitCommand extends Command {
|
||||
}
|
||||
|
||||
// Get changed files for scope detection
|
||||
const status = await gitAdapter.getStatus();
|
||||
const changedFiles = [...status.staged, ...status.modified];
|
||||
const gitStatus = await gitAdapter.getStatus();
|
||||
const changedFiles = [...gitStatus.staged, ...gitStatus.modified];
|
||||
|
||||
// Generate commit message
|
||||
const messageGenerator = createCommitMessageGenerator();
|
||||
const testResults = state.context.lastTestResults;
|
||||
const messageGenerator = new CommitMessageGenerator();
|
||||
const testResults = workflowContext.lastTestResults;
|
||||
|
||||
const commitMessage = messageGenerator.generateMessage({
|
||||
type: 'feat',
|
||||
description: currentSubtask.title,
|
||||
description: status.currentSubtask.title,
|
||||
changedFiles,
|
||||
taskId: state.context.taskId,
|
||||
phase: 'TDD',
|
||||
tag: (state.context.metadata.tag as string) || undefined,
|
||||
taskId: status.taskId,
|
||||
phase: status.tddPhase,
|
||||
tag: (workflowContext.metadata.tag as string) || undefined,
|
||||
testsPassing: testResults?.passed,
|
||||
testsFailing: testResults?.failed,
|
||||
coveragePercent: undefined // Could be added if available
|
||||
@@ -117,8 +103,8 @@ export class CommitCommand extends Command {
|
||||
// Create commit with metadata
|
||||
await gitAdapter.createCommit(commitMessage, {
|
||||
metadata: {
|
||||
taskId: state.context.taskId,
|
||||
subtaskId: currentSubtask.id,
|
||||
taskId: status.taskId,
|
||||
subtaskId: status.currentSubtask.id,
|
||||
phase: 'COMMIT',
|
||||
tddCycle: 'complete'
|
||||
}
|
||||
@@ -127,36 +113,24 @@ export class CommitCommand extends Command {
|
||||
// Get commit info
|
||||
const lastCommit = await gitAdapter.getLastCommit();
|
||||
|
||||
// Complete COMMIT phase (this marks subtask as completed)
|
||||
orchestrator.transition({ type: 'COMMIT_COMPLETE' });
|
||||
// Complete COMMIT phase and advance workflow
|
||||
// This handles all transitions internally with a single WorkflowStateManager
|
||||
const newStatus = await workflowService.commit();
|
||||
|
||||
// Check if should advance to next subtask
|
||||
const progress = orchestrator.getProgress();
|
||||
if (progress.current < progress.total) {
|
||||
orchestrator.transition({ type: 'SUBTASK_COMPLETE' });
|
||||
} else {
|
||||
// All subtasks complete
|
||||
orchestrator.transition({ type: 'ALL_SUBTASKS_COMPLETE' });
|
||||
}
|
||||
const isComplete = newStatus.phase === 'COMPLETE';
|
||||
|
||||
// Output success
|
||||
formatter.success('Commit created', {
|
||||
commitHash: lastCommit.hash.substring(0, 7),
|
||||
message: commitMessage.split('\n')[0], // First line only
|
||||
subtask: {
|
||||
id: currentSubtask.id,
|
||||
title: currentSubtask.title,
|
||||
status: currentSubtask.status
|
||||
id: status.currentSubtask.id,
|
||||
title: status.currentSubtask.title
|
||||
},
|
||||
progress: {
|
||||
completed: progress.completed,
|
||||
total: progress.total,
|
||||
percentage: progress.percentage
|
||||
},
|
||||
nextAction:
|
||||
progress.completed < progress.total
|
||||
? 'Start next subtask with RED phase'
|
||||
: 'All subtasks complete. Run: autopilot status'
|
||||
progress: newStatus.progress,
|
||||
nextAction: isComplete
|
||||
? 'All subtasks complete. Run: autopilot status'
|
||||
: 'Start next subtask with RED phase'
|
||||
});
|
||||
} catch (error) {
|
||||
formatter.error((error as Error).message);
|
||||
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -64,6 +64,7 @@
|
||||
"open": "^10.2.0",
|
||||
"ora": "^8.2.0",
|
||||
"simple-git": "^3.28.0",
|
||||
"steno": "^4.0.2",
|
||||
"uuid": "^11.1.0",
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
@@ -25049,6 +25050,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/steno": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/steno/-/steno-4.0.2.tgz",
|
||||
"integrity": "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/typicode"
|
||||
}
|
||||
},
|
||||
"node_modules/stop-iteration-iterator": {
|
||||
"version": "1.1.0",
|
||||
"dev": true,
|
||||
@@ -28500,6 +28513,7 @@
|
||||
"@supabase/supabase-js": "^2.57.4",
|
||||
"fs-extra": "^11.3.2",
|
||||
"simple-git": "^3.28.0",
|
||||
"steno": "^4.0.2",
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
"open": "^10.2.0",
|
||||
"ora": "^8.2.0",
|
||||
"simple-git": "^3.28.0",
|
||||
"steno": "^4.0.2",
|
||||
"uuid": "^11.1.0",
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"@supabase/supabase-js": "^2.57.4",
|
||||
"fs-extra": "^11.3.2",
|
||||
"simple-git": "^3.28.0",
|
||||
"steno": "^4.0.2",
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import { Writer } from 'steno';
|
||||
import type { WorkflowState } from '../types.js';
|
||||
import { getLogger } from '../../../common/logger/index.js';
|
||||
|
||||
@@ -28,7 +29,8 @@ export class WorkflowStateManager {
|
||||
private readonly sessionDir: string;
|
||||
private maxBackups: number;
|
||||
private readonly logger = getLogger('WorkflowStateManager');
|
||||
private savePromise: Promise<void> | null = null;
|
||||
private writer: Writer | null = null;
|
||||
private writerInitPromise: Promise<void> | null = null;
|
||||
|
||||
constructor(projectRoot: string, maxBackups = 5) {
|
||||
this.projectRoot = path.resolve(projectRoot);
|
||||
@@ -70,6 +72,31 @@ export class WorkflowStateManager {
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the steno Writer is initialized
|
||||
* This ensures the session directory exists before creating the writer
|
||||
*/
|
||||
private async ensureWriter(): Promise<void> {
|
||||
if (this.writer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If another call is already initializing, wait for it
|
||||
if (this.writerInitPromise) {
|
||||
await this.writerInitPromise;
|
||||
return;
|
||||
}
|
||||
|
||||
this.writerInitPromise = (async () => {
|
||||
// Ensure session directory exists before creating writer
|
||||
await fs.mkdir(this.sessionDir, { recursive: true });
|
||||
this.writer = new Writer(this.statePath);
|
||||
})();
|
||||
|
||||
await this.writerInitPromise;
|
||||
this.writerInitPromise = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if workflow state exists
|
||||
*/
|
||||
@@ -99,40 +126,12 @@ export class WorkflowStateManager {
|
||||
|
||||
/**
|
||||
* Save workflow state to disk
|
||||
* Uses a mutex to prevent concurrent saves from corrupting the file
|
||||
* Uses steno for atomic writes and automatic queueing of concurrent saves
|
||||
*/
|
||||
async save(state: WorkflowState): Promise<void> {
|
||||
// Chain this save after any pending operation atomically
|
||||
const previousSave = this.savePromise;
|
||||
const currentSave = (async () => {
|
||||
if (previousSave) {
|
||||
await previousSave;
|
||||
}
|
||||
await this.performSave(state);
|
||||
})();
|
||||
|
||||
this.savePromise = currentSave;
|
||||
try {
|
||||
await currentSave;
|
||||
} finally {
|
||||
// Only clear if we're still the active save
|
||||
if (this.savePromise === currentSave) {
|
||||
this.savePromise = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method that performs the actual save operation
|
||||
*/
|
||||
private async performSave(state: WorkflowState): Promise<void> {
|
||||
// Use unique temp path to avoid conflicts
|
||||
const timestamp = Date.now();
|
||||
const tempPath = `${this.statePath}.${timestamp}.tmp`;
|
||||
|
||||
try {
|
||||
// Ensure session directory exists
|
||||
await fs.mkdir(this.sessionDir, { recursive: true });
|
||||
// Ensure writer is initialized (creates directory if needed)
|
||||
await this.ensureWriter();
|
||||
|
||||
// Serialize and validate JSON
|
||||
const jsonContent = JSON.stringify(state, null, 2);
|
||||
@@ -145,18 +144,11 @@ export class WorkflowStateManager {
|
||||
throw new Error('Failed to generate valid JSON from workflow state');
|
||||
}
|
||||
|
||||
// Write state atomically with newline at end
|
||||
await fs.writeFile(tempPath, jsonContent + '\n', 'utf-8');
|
||||
await fs.rename(tempPath, this.statePath);
|
||||
// Write using steno (handles queuing and atomic writes automatically)
|
||||
await this.writer!.write(jsonContent + '\n');
|
||||
|
||||
this.logger.debug(`Saved workflow state (${jsonContent.length} bytes)`);
|
||||
} catch (error: any) {
|
||||
// Clean up temp file if it exists
|
||||
try {
|
||||
await fs.unlink(tempPath);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
throw new Error(`Failed to save workflow state: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user