mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 06:12:06 +00:00
Add in-progress status tracking for features
Implements feature locking to prevent multiple agent sessions from working on the same feature simultaneously. This is essential for parallel agent execution. Database changes: - Add `in_progress` boolean column to Feature model - Add migration function to handle existing databases MCP Server tools: - Add `feature_mark_in_progress` - lock feature when starting work - Add `feature_clear_in_progress` - unlock feature when abandoning - Update `feature_get_next` to skip in-progress features - Update `feature_get_stats` to include in_progress count - Update `feature_mark_passing` and `feature_skip` to clear in_progress Backend updates: - Update progress.py to track and display in_progress count - Update features router to properly categorize in-progress features - Update WebSocket to broadcast in_progress in progress updates - Add in_progress to FeatureResponse schema Frontend updates: - Add in_progress to TypeScript types (Feature, ProjectStats, WSProgressMessage) - Update useWebSocket hook to track in_progress state Prompt template: - Add instructions for agents to mark features in-progress immediately - Document new MCP tools in allowed tools section Also fixes spec_chat_session.py to use absolute project path instead of relative path for consistency with CLI behavior. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -88,6 +88,13 @@ Get the next feature to implement:
|
|||||||
Use the feature_get_next tool
|
Use the feature_get_next tool
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Once you've retrieved the feature, **immediately mark it as in-progress**:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Mark feature as in-progress to prevent other sessions from working on it
|
||||||
|
Use the feature_mark_in_progress tool with feature_id=42
|
||||||
|
```
|
||||||
|
|
||||||
Focus on completing one feature perfectly and completing its testing steps in this session before moving on to other features.
|
Focus on completing one feature perfectly and completing its testing steps in this session before moving on to other features.
|
||||||
It's ok if you only complete one feature in this session, as there will be more sessions later that continue to make progress.
|
It's ok if you only complete one feature in this session, as there will be more sessions later that continue to make progress.
|
||||||
|
|
||||||
@@ -335,20 +342,26 @@ The feature tools exist to reduce token usage. **DO NOT make exploratory queries
|
|||||||
### ALLOWED Feature Tools (ONLY these):
|
### ALLOWED Feature Tools (ONLY these):
|
||||||
|
|
||||||
```
|
```
|
||||||
# 1. Get progress stats (passing/total counts)
|
# 1. Get progress stats (passing/in_progress/total counts)
|
||||||
feature_get_stats
|
feature_get_stats
|
||||||
|
|
||||||
# 2. Get the NEXT feature to work on (one feature only)
|
# 2. Get the NEXT feature to work on (one feature only)
|
||||||
feature_get_next
|
feature_get_next
|
||||||
|
|
||||||
# 3. Get up to 3 random passing features for regression testing
|
# 3. Mark a feature as in-progress (call immediately after feature_get_next)
|
||||||
|
feature_mark_in_progress with feature_id={id}
|
||||||
|
|
||||||
|
# 4. Get up to 3 random passing features for regression testing
|
||||||
feature_get_for_regression
|
feature_get_for_regression
|
||||||
|
|
||||||
# 4. Mark a feature as passing (after verification)
|
# 5. Mark a feature as passing (after verification)
|
||||||
feature_mark_passing with feature_id={id}
|
feature_mark_passing with feature_id={id}
|
||||||
|
|
||||||
# 5. Skip a feature (moves to end of queue) - ONLY when blocked by dependency
|
# 6. Skip a feature (moves to end of queue) - ONLY when blocked by dependency
|
||||||
feature_skip with feature_id={id}
|
feature_skip with feature_id={id}
|
||||||
|
|
||||||
|
# 7. Clear in-progress status (when abandoning a feature)
|
||||||
|
feature_clear_in_progress with feature_id={id}
|
||||||
```
|
```
|
||||||
|
|
||||||
### RULES:
|
### RULES:
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class Feature(Base):
|
|||||||
description = Column(Text, nullable=False)
|
description = Column(Text, nullable=False)
|
||||||
steps = Column(JSON, nullable=False) # Stored as JSON array
|
steps = Column(JSON, nullable=False) # Stored as JSON array
|
||||||
passes = Column(Boolean, default=False, index=True)
|
passes = Column(Boolean, default=False, index=True)
|
||||||
|
in_progress = Column(Boolean, default=False, index=True)
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
"""Convert feature to dictionary for JSON serialization."""
|
"""Convert feature to dictionary for JSON serialization."""
|
||||||
@@ -39,6 +40,7 @@ class Feature(Base):
|
|||||||
"description": self.description,
|
"description": self.description,
|
||||||
"steps": self.steps,
|
"steps": self.steps,
|
||||||
"passes": self.passes,
|
"passes": self.passes,
|
||||||
|
"in_progress": self.in_progress,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -56,6 +58,21 @@ def get_database_url(project_dir: Path) -> str:
|
|||||||
return f"sqlite:///{db_path.as_posix()}"
|
return f"sqlite:///{db_path.as_posix()}"
|
||||||
|
|
||||||
|
|
||||||
|
def _migrate_add_in_progress_column(engine) -> None:
|
||||||
|
"""Add in_progress column to existing databases that don't have it."""
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
with engine.connect() as conn:
|
||||||
|
# Check if column exists
|
||||||
|
result = conn.execute(text("PRAGMA table_info(features)"))
|
||||||
|
columns = [row[1] for row in result.fetchall()]
|
||||||
|
|
||||||
|
if "in_progress" not in columns:
|
||||||
|
# Add the column with default value
|
||||||
|
conn.execute(text("ALTER TABLE features ADD COLUMN in_progress BOOLEAN DEFAULT 0"))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
def create_database(project_dir: Path) -> tuple:
|
def create_database(project_dir: Path) -> tuple:
|
||||||
"""
|
"""
|
||||||
Create database and return engine + session maker.
|
Create database and return engine + session maker.
|
||||||
@@ -69,6 +86,10 @@ def create_database(project_dir: Path) -> tuple:
|
|||||||
db_url = get_database_url(project_dir)
|
db_url = get_database_url(project_dir)
|
||||||
engine = create_engine(db_url, connect_args={"check_same_thread": False})
|
engine = create_engine(db_url, connect_args={"check_same_thread": False})
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
# Migrate existing databases to add in_progress column
|
||||||
|
_migrate_add_in_progress_column(engine)
|
||||||
|
|
||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
return engine, SessionLocal
|
return engine, SessionLocal
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ FEATURE_MCP_TOOLS = [
|
|||||||
"mcp__features__feature_get_stats",
|
"mcp__features__feature_get_stats",
|
||||||
"mcp__features__feature_get_next",
|
"mcp__features__feature_get_next",
|
||||||
"mcp__features__feature_get_for_regression",
|
"mcp__features__feature_get_for_regression",
|
||||||
|
"mcp__features__feature_mark_in_progress",
|
||||||
"mcp__features__feature_mark_passing",
|
"mcp__features__feature_mark_passing",
|
||||||
"mcp__features__feature_skip",
|
"mcp__features__feature_skip",
|
||||||
"mcp__features__feature_create_bulk",
|
"mcp__features__feature_create_bulk",
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ Tools:
|
|||||||
- feature_get_for_regression: Get random passing features for testing
|
- feature_get_for_regression: Get random passing features for testing
|
||||||
- feature_mark_passing: Mark a feature as passing
|
- feature_mark_passing: Mark a feature as passing
|
||||||
- feature_skip: Skip a feature (move to end of queue)
|
- feature_skip: Skip a feature (move to end of queue)
|
||||||
|
- feature_mark_in_progress: Mark a feature as in-progress
|
||||||
|
- feature_clear_in_progress: Clear in-progress status
|
||||||
- feature_create_bulk: Create multiple features at once
|
- feature_create_bulk: Create multiple features at once
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -47,6 +49,16 @@ class SkipFeatureInput(BaseModel):
|
|||||||
feature_id: int = Field(..., description="The ID of the feature to skip", ge=1)
|
feature_id: int = Field(..., description="The ID of the feature to skip", ge=1)
|
||||||
|
|
||||||
|
|
||||||
|
class MarkInProgressInput(BaseModel):
|
||||||
|
"""Input for marking a feature as in-progress."""
|
||||||
|
feature_id: int = Field(..., description="The ID of the feature to mark as in-progress", ge=1)
|
||||||
|
|
||||||
|
|
||||||
|
class ClearInProgressInput(BaseModel):
|
||||||
|
"""Input for clearing in-progress status."""
|
||||||
|
feature_id: int = Field(..., description="The ID of the feature to clear in-progress status", ge=1)
|
||||||
|
|
||||||
|
|
||||||
class RegressionInput(BaseModel):
|
class RegressionInput(BaseModel):
|
||||||
"""Input for getting regression features."""
|
"""Input for getting regression features."""
|
||||||
limit: int = Field(default=3, ge=1, le=10, description="Maximum number of passing features to return")
|
limit: int = Field(default=3, ge=1, le=10, description="Maximum number of passing features to return")
|
||||||
@@ -106,20 +118,22 @@ def get_session():
|
|||||||
def feature_get_stats() -> str:
|
def feature_get_stats() -> str:
|
||||||
"""Get statistics about feature completion progress.
|
"""Get statistics about feature completion progress.
|
||||||
|
|
||||||
Returns the number of passing features, total features, and completion percentage.
|
Returns the number of passing features, in-progress features, total features,
|
||||||
Use this to track overall progress of the implementation.
|
and completion percentage. Use this to track overall progress of the implementation.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
JSON with: passing (int), total (int), percentage (float)
|
JSON with: passing (int), in_progress (int), total (int), percentage (float)
|
||||||
"""
|
"""
|
||||||
session = get_session()
|
session = get_session()
|
||||||
try:
|
try:
|
||||||
total = session.query(Feature).count()
|
total = session.query(Feature).count()
|
||||||
passing = session.query(Feature).filter(Feature.passes == True).count()
|
passing = session.query(Feature).filter(Feature.passes == True).count()
|
||||||
|
in_progress = session.query(Feature).filter(Feature.in_progress == True).count()
|
||||||
percentage = round((passing / total) * 100, 1) if total > 0 else 0.0
|
percentage = round((passing / total) * 100, 1) if total > 0 else 0.0
|
||||||
|
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
"passing": passing,
|
"passing": passing,
|
||||||
|
"in_progress": in_progress,
|
||||||
"total": total,
|
"total": total,
|
||||||
"percentage": percentage
|
"percentage": percentage
|
||||||
}, indent=2)
|
}, indent=2)
|
||||||
@@ -131,24 +145,26 @@ def feature_get_stats() -> str:
|
|||||||
def feature_get_next() -> str:
|
def feature_get_next() -> str:
|
||||||
"""Get the highest-priority pending feature to work on.
|
"""Get the highest-priority pending feature to work on.
|
||||||
|
|
||||||
Returns the feature with the lowest priority number that has passes=false.
|
Returns the feature with the lowest priority number that has passes=false
|
||||||
|
and is not currently in-progress by another session.
|
||||||
Use this at the start of each coding session to determine what to implement next.
|
Use this at the start of each coding session to determine what to implement next.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
JSON with feature details (id, priority, category, name, description, steps, passes)
|
JSON with feature details (id, priority, category, name, description, steps, passes, in_progress)
|
||||||
or error message if all features are passing.
|
or error message if all features are passing or in-progress.
|
||||||
"""
|
"""
|
||||||
session = get_session()
|
session = get_session()
|
||||||
try:
|
try:
|
||||||
feature = (
|
feature = (
|
||||||
session.query(Feature)
|
session.query(Feature)
|
||||||
.filter(Feature.passes == False)
|
.filter(Feature.passes == False)
|
||||||
|
.filter(Feature.in_progress == False)
|
||||||
.order_by(Feature.priority.asc(), Feature.id.asc())
|
.order_by(Feature.priority.asc(), Feature.id.asc())
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
if feature is None:
|
if feature is None:
|
||||||
return json.dumps({"error": "All features are passing! No more work to do."})
|
return json.dumps({"error": "All features are passing or in-progress! No more work to do."})
|
||||||
|
|
||||||
return json.dumps(feature.to_dict(), indent=2)
|
return json.dumps(feature.to_dict(), indent=2)
|
||||||
finally:
|
finally:
|
||||||
@@ -195,8 +211,8 @@ def feature_mark_passing(
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""Mark a feature as passing after successful implementation.
|
"""Mark a feature as passing after successful implementation.
|
||||||
|
|
||||||
Updates the feature's passes field to true. Use this after you have
|
Updates the feature's passes field to true and clears the in_progress flag.
|
||||||
implemented the feature and verified it works correctly.
|
Use this after you have implemented the feature and verified it works correctly.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
feature_id: The ID of the feature to mark as passing
|
feature_id: The ID of the feature to mark as passing
|
||||||
@@ -212,6 +228,7 @@ def feature_mark_passing(
|
|||||||
return json.dumps({"error": f"Feature with ID {feature_id} not found"})
|
return json.dumps({"error": f"Feature with ID {feature_id} not found"})
|
||||||
|
|
||||||
feature.passes = True
|
feature.passes = True
|
||||||
|
feature.in_progress = False
|
||||||
session.commit()
|
session.commit()
|
||||||
session.refresh(feature)
|
session.refresh(feature)
|
||||||
|
|
||||||
@@ -232,7 +249,8 @@ def feature_skip(
|
|||||||
- Technical prerequisites that need to be addressed first
|
- Technical prerequisites that need to be addressed first
|
||||||
|
|
||||||
The feature's priority is set to max_priority + 1, so it will be
|
The feature's priority is set to max_priority + 1, so it will be
|
||||||
worked on after all other pending features.
|
worked on after all other pending features. Also clears the in_progress
|
||||||
|
flag so the feature returns to "pending" status.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
feature_id: The ID of the feature to skip
|
feature_id: The ID of the feature to skip
|
||||||
@@ -257,6 +275,7 @@ def feature_skip(
|
|||||||
new_priority = (max_priority_result[0] + 1) if max_priority_result else 1
|
new_priority = (max_priority_result[0] + 1) if max_priority_result else 1
|
||||||
|
|
||||||
feature.priority = new_priority
|
feature.priority = new_priority
|
||||||
|
feature.in_progress = False
|
||||||
session.commit()
|
session.commit()
|
||||||
session.refresh(feature)
|
session.refresh(feature)
|
||||||
|
|
||||||
@@ -271,6 +290,74 @@ def feature_skip(
|
|||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def feature_mark_in_progress(
|
||||||
|
feature_id: Annotated[int, Field(description="The ID of the feature to mark as in-progress", ge=1)]
|
||||||
|
) -> str:
|
||||||
|
"""Mark a feature as in-progress. Call immediately after feature_get_next().
|
||||||
|
|
||||||
|
This prevents other agent sessions from working on the same feature.
|
||||||
|
Use this as soon as you retrieve a feature to work on.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
feature_id: The ID of the feature to mark as in-progress
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON with the updated feature details, or error if not found or already in-progress.
|
||||||
|
"""
|
||||||
|
session = get_session()
|
||||||
|
try:
|
||||||
|
feature = session.query(Feature).filter(Feature.id == feature_id).first()
|
||||||
|
|
||||||
|
if feature is None:
|
||||||
|
return json.dumps({"error": f"Feature with ID {feature_id} not found"})
|
||||||
|
|
||||||
|
if feature.passes:
|
||||||
|
return json.dumps({"error": f"Feature with ID {feature_id} is already passing"})
|
||||||
|
|
||||||
|
if feature.in_progress:
|
||||||
|
return json.dumps({"error": f"Feature with ID {feature_id} is already in-progress"})
|
||||||
|
|
||||||
|
feature.in_progress = True
|
||||||
|
session.commit()
|
||||||
|
session.refresh(feature)
|
||||||
|
|
||||||
|
return json.dumps(feature.to_dict(), indent=2)
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def feature_clear_in_progress(
|
||||||
|
feature_id: Annotated[int, Field(description="The ID of the feature to clear in-progress status", ge=1)]
|
||||||
|
) -> str:
|
||||||
|
"""Clear in-progress status from a feature.
|
||||||
|
|
||||||
|
Use this when abandoning a feature or manually unsticking a stuck feature.
|
||||||
|
The feature will return to the pending queue.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
feature_id: The ID of the feature to clear in-progress status
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON with the updated feature details, or error if not found.
|
||||||
|
"""
|
||||||
|
session = get_session()
|
||||||
|
try:
|
||||||
|
feature = session.query(Feature).filter(Feature.id == feature_id).first()
|
||||||
|
|
||||||
|
if feature is None:
|
||||||
|
return json.dumps({"error": f"Feature with ID {feature_id} not found"})
|
||||||
|
|
||||||
|
feature.in_progress = False
|
||||||
|
session.commit()
|
||||||
|
session.refresh(feature)
|
||||||
|
|
||||||
|
return json.dumps(feature.to_dict(), indent=2)
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def feature_create_bulk(
|
def feature_create_bulk(
|
||||||
features: Annotated[list[dict], Field(description="List of features to create, each with category, name, description, and steps")]
|
features: Annotated[list[dict], Field(description="List of features to create, each with category, name, description, and steps")]
|
||||||
|
|||||||
25
progress.py
25
progress.py
@@ -56,19 +56,19 @@ def has_features(project_dir: Path) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def count_passing_tests(project_dir: Path) -> tuple[int, int]:
|
def count_passing_tests(project_dir: Path) -> tuple[int, int, int]:
|
||||||
"""
|
"""
|
||||||
Count passing and total tests via direct database access.
|
Count passing, in_progress, and total tests via direct database access.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
project_dir: Directory containing the project
|
project_dir: Directory containing the project
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(passing_count, total_count)
|
(passing_count, in_progress_count, total_count)
|
||||||
"""
|
"""
|
||||||
db_file = project_dir / "features.db"
|
db_file = project_dir / "features.db"
|
||||||
if not db_file.exists():
|
if not db_file.exists():
|
||||||
return 0, 0
|
return 0, 0, 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
conn = sqlite3.connect(db_file)
|
conn = sqlite3.connect(db_file)
|
||||||
@@ -77,11 +77,17 @@ def count_passing_tests(project_dir: Path) -> tuple[int, int]:
|
|||||||
total = cursor.fetchone()[0]
|
total = cursor.fetchone()[0]
|
||||||
cursor.execute("SELECT COUNT(*) FROM features WHERE passes = 1")
|
cursor.execute("SELECT COUNT(*) FROM features WHERE passes = 1")
|
||||||
passing = cursor.fetchone()[0]
|
passing = cursor.fetchone()[0]
|
||||||
|
# Handle case where in_progress column doesn't exist yet
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM features WHERE in_progress = 1")
|
||||||
|
in_progress = cursor.fetchone()[0]
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
in_progress = 0
|
||||||
conn.close()
|
conn.close()
|
||||||
return passing, total
|
return passing, in_progress, total
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[Database error in count_passing_tests: {e}]")
|
print(f"[Database error in count_passing_tests: {e}]")
|
||||||
return 0, 0
|
return 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
def get_all_passing_features(project_dir: Path) -> list[dict]:
|
def get_all_passing_features(project_dir: Path) -> list[dict]:
|
||||||
@@ -205,11 +211,14 @@ def print_session_header(session_num: int, is_initializer: bool) -> None:
|
|||||||
|
|
||||||
def print_progress_summary(project_dir: Path) -> None:
|
def print_progress_summary(project_dir: Path) -> None:
|
||||||
"""Print a summary of current progress."""
|
"""Print a summary of current progress."""
|
||||||
passing, total = count_passing_tests(project_dir)
|
passing, in_progress, total = count_passing_tests(project_dir)
|
||||||
|
|
||||||
if total > 0:
|
if total > 0:
|
||||||
percentage = (passing / total) * 100
|
percentage = (passing / total) * 100
|
||||||
print(f"\nProgress: {passing}/{total} tests passing ({percentage:.1f}%)")
|
status_parts = [f"{passing}/{total} tests passing ({percentage:.1f}%)"]
|
||||||
|
if in_progress > 0:
|
||||||
|
status_parts.append(f"{in_progress} in progress")
|
||||||
|
print(f"\nProgress: {', '.join(status_parts)}")
|
||||||
send_progress_webhook(passing, total, project_dir)
|
send_progress_webhook(passing, total, project_dir)
|
||||||
else:
|
else:
|
||||||
print("\nProgress: No features in database yet")
|
print("\nProgress: No features in database yet")
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ def feature_to_response(f) -> FeatureResponse:
|
|||||||
description=f.description,
|
description=f.description,
|
||||||
steps=f.steps if isinstance(f.steps, list) else [],
|
steps=f.steps if isinstance(f.steps, list) else [],
|
||||||
passes=f.passes,
|
passes=f.passes,
|
||||||
|
in_progress=f.in_progress,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -123,18 +124,21 @@ async def list_features(project_name: str):
|
|||||||
all_features = session.query(Feature).order_by(Feature.priority).all()
|
all_features = session.query(Feature).order_by(Feature.priority).all()
|
||||||
|
|
||||||
pending = []
|
pending = []
|
||||||
|
in_progress = []
|
||||||
done = []
|
done = []
|
||||||
|
|
||||||
for f in all_features:
|
for f in all_features:
|
||||||
feature_response = feature_to_response(f)
|
feature_response = feature_to_response(f)
|
||||||
if f.passes:
|
if f.passes:
|
||||||
done.append(feature_response)
|
done.append(feature_response)
|
||||||
|
elif f.in_progress:
|
||||||
|
in_progress.append(feature_response)
|
||||||
else:
|
else:
|
||||||
pending.append(feature_response)
|
pending.append(feature_response)
|
||||||
|
|
||||||
return FeatureListResponse(
|
return FeatureListResponse(
|
||||||
pending=pending,
|
pending=pending,
|
||||||
in_progress=[],
|
in_progress=in_progress,
|
||||||
done=done,
|
done=done,
|
||||||
)
|
)
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ def validate_project_name(name: str) -> str:
|
|||||||
def get_project_stats(project_dir: Path) -> ProjectStats:
|
def get_project_stats(project_dir: Path) -> ProjectStats:
|
||||||
"""Get statistics for a project."""
|
"""Get statistics for a project."""
|
||||||
_init_imports()
|
_init_imports()
|
||||||
passing, total = _count_passing_tests(project_dir)
|
passing, _, total = _count_passing_tests(project_dir)
|
||||||
percentage = (passing / total * 100) if total > 0 else 0.0
|
percentage = (passing / total * 100) if total > 0 else 0.0
|
||||||
return ProjectStats(passing=passing, total=total, percentage=round(percentage, 1))
|
return ProjectStats(passing=passing, total=total, percentage=round(percentage, 1))
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ class FeatureResponse(FeatureBase):
|
|||||||
id: int
|
id: int
|
||||||
priority: int
|
priority: int
|
||||||
passes: bool
|
passes: bool
|
||||||
|
in_progress: bool
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|||||||
@@ -83,8 +83,12 @@ class SpecChatSession:
|
|||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
skill_content = skill_path.read_text(encoding="utf-8", errors="replace")
|
skill_content = skill_path.read_text(encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
# Replace $ARGUMENTS with the project path (use forward slashes for consistency)
|
# Ensure project directory exists (like CLI does in start.py)
|
||||||
project_path = f"generations/{self.project_name}"
|
self.project_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Replace $ARGUMENTS with absolute project path (like CLI does in start.py:184)
|
||||||
|
# Using absolute path avoids confusion when project folder name differs from app name
|
||||||
|
project_path = str(self.project_dir.resolve())
|
||||||
system_prompt = skill_content.replace("$ARGUMENTS", project_path)
|
system_prompt = skill_content.replace("$ARGUMENTS", project_path)
|
||||||
|
|
||||||
# Create Claude SDK client with limited tools for spec creation
|
# Create Claude SDK client with limited tools for spec creation
|
||||||
|
|||||||
@@ -117,21 +117,24 @@ async def poll_progress(websocket: WebSocket, project_name: str):
|
|||||||
project_dir = _get_generations_dir() / project_name
|
project_dir = _get_generations_dir() / project_name
|
||||||
count_passing_tests = _get_count_passing_tests()
|
count_passing_tests = _get_count_passing_tests()
|
||||||
last_passing = -1
|
last_passing = -1
|
||||||
|
last_in_progress = -1
|
||||||
last_total = -1
|
last_total = -1
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
passing, total = count_passing_tests(project_dir)
|
passing, in_progress, total = count_passing_tests(project_dir)
|
||||||
|
|
||||||
# Only send if changed
|
# Only send if changed
|
||||||
if passing != last_passing or total != last_total:
|
if passing != last_passing or in_progress != last_in_progress or total != last_total:
|
||||||
last_passing = passing
|
last_passing = passing
|
||||||
|
last_in_progress = in_progress
|
||||||
last_total = total
|
last_total = total
|
||||||
percentage = (passing / total * 100) if total > 0 else 0
|
percentage = (passing / total * 100) if total > 0 else 0
|
||||||
|
|
||||||
await websocket.send_json({
|
await websocket.send_json({
|
||||||
"type": "progress",
|
"type": "progress",
|
||||||
"passing": passing,
|
"passing": passing,
|
||||||
|
"in_progress": in_progress,
|
||||||
"total": total,
|
"total": total,
|
||||||
"percentage": round(percentage, 1),
|
"percentage": round(percentage, 1),
|
||||||
})
|
})
|
||||||
@@ -204,11 +207,12 @@ async def project_websocket(websocket: WebSocket, project_name: str):
|
|||||||
|
|
||||||
# Send initial progress
|
# Send initial progress
|
||||||
count_passing_tests = _get_count_passing_tests()
|
count_passing_tests = _get_count_passing_tests()
|
||||||
passing, total = count_passing_tests(project_dir)
|
passing, in_progress, total = count_passing_tests(project_dir)
|
||||||
percentage = (passing / total * 100) if total > 0 else 0
|
percentage = (passing / total * 100) if total > 0 else 0
|
||||||
await websocket.send_json({
|
await websocket.send_json({
|
||||||
"type": "progress",
|
"type": "progress",
|
||||||
"passing": passing,
|
"passing": passing,
|
||||||
|
"in_progress": in_progress,
|
||||||
"total": total,
|
"total": total,
|
||||||
"percentage": round(percentage, 1),
|
"percentage": round(percentage, 1),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type { WSMessage, AgentStatus } from '../lib/types'
|
|||||||
interface WebSocketState {
|
interface WebSocketState {
|
||||||
progress: {
|
progress: {
|
||||||
passing: number
|
passing: number
|
||||||
|
in_progress: number
|
||||||
total: number
|
total: number
|
||||||
percentage: number
|
percentage: number
|
||||||
}
|
}
|
||||||
@@ -20,7 +21,7 @@ const MAX_LOGS = 100 // Keep last 100 log lines
|
|||||||
|
|
||||||
export function useProjectWebSocket(projectName: string | null) {
|
export function useProjectWebSocket(projectName: string | null) {
|
||||||
const [state, setState] = useState<WebSocketState>({
|
const [state, setState] = useState<WebSocketState>({
|
||||||
progress: { passing: 0, total: 0, percentage: 0 },
|
progress: { passing: 0, in_progress: 0, total: 0, percentage: 0 },
|
||||||
agentStatus: 'stopped',
|
agentStatus: 'stopped',
|
||||||
logs: [],
|
logs: [],
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
@@ -57,6 +58,7 @@ export function useProjectWebSocket(projectName: string | null) {
|
|||||||
...prev,
|
...prev,
|
||||||
progress: {
|
progress: {
|
||||||
passing: message.passing,
|
passing: message.passing,
|
||||||
|
in_progress: message.in_progress,
|
||||||
total: message.total,
|
total: message.total,
|
||||||
percentage: message.percentage,
|
percentage: message.percentage,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
// Project types
|
// Project types
|
||||||
export interface ProjectStats {
|
export interface ProjectStats {
|
||||||
passing: number
|
passing: number
|
||||||
|
in_progress: number
|
||||||
total: number
|
total: number
|
||||||
percentage: number
|
percentage: number
|
||||||
}
|
}
|
||||||
@@ -34,6 +35,7 @@ export interface Feature {
|
|||||||
description: string
|
description: string
|
||||||
steps: string[]
|
steps: string[]
|
||||||
passes: boolean
|
passes: boolean
|
||||||
|
in_progress: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FeatureListResponse {
|
export interface FeatureListResponse {
|
||||||
@@ -79,6 +81,7 @@ export type WSMessageType = 'progress' | 'feature_update' | 'log' | 'agent_statu
|
|||||||
export interface WSProgressMessage {
|
export interface WSProgressMessage {
|
||||||
type: 'progress'
|
type: 'progress'
|
||||||
passing: number
|
passing: number
|
||||||
|
in_progress: number
|
||||||
total: number
|
total: number
|
||||||
percentage: number
|
percentage: number
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user