Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ The `specify` command supports the following options:
|-------------|----------------------------------------------------------------|
| `init` | Initialize a new Specify project from the latest template |
| `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `qwen`, `opencode`, `codex`, `shai`) |
| `config` | Manage Specify project configuration (progress tracking settings, etc.) |
| `version` | Display version and system information |

### `specify init` Arguments & Options

Expand Down Expand Up @@ -223,6 +225,12 @@ specify init my-project --ai claude --github-token ghp_your_token_here

# Check system requirements
specify check

# View and manage configuration
specify config # Show current configuration
specify config --auto-tracking # Enable auto-tracking
specify config --no-auto-tracking # Disable auto-tracking
specify config --get progress.autoTracking # Get specific setting
```

### Available Slash Commands
Expand Down
202 changes: 202 additions & 0 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1349,6 +1349,208 @@ def version():
console.print(panel)
console.print()

def _get_config_path(project_path: Path = None) -> Path:
"""Get the path to .specify/config.json file."""
if project_path is None:
project_path = Path.cwd()
return project_path / ".specify" / "config.json"

def _load_config(project_path: Path = None) -> dict:
"""Load configuration from .specify/config.json, return default if not exists."""
config_path = _get_config_path(project_path)
default_config = {
"progress": {
"autoTracking": False,
"updateOnTaskComplete": True,
"updateOnPhaseComplete": True
},
"version": "1.0"
}

if not config_path.exists():
return default_config

try:
with open(config_path, 'r', encoding='utf-8') as f:
user_config = json.load(f)
# Merge with defaults to ensure all keys exist
merged = default_config.copy()
if "progress" in user_config:
merged["progress"].update(user_config["progress"])
if "version" in user_config:
merged["version"] = user_config["version"]
return merged
except (json.JSONDecodeError, IOError) as e:
console.print(f"[yellow]Warning:[/yellow] Could not read config file: {e}")
console.print("[dim]Using default configuration[/dim]")
return default_config

def _save_config(config: dict, project_path: Path = None) -> bool:
"""Save configuration to .specify/config.json."""
config_path = _get_config_path(project_path)
config_dir = config_path.parent

try:
# Ensure .specify directory exists
config_dir.mkdir(parents=True, exist_ok=True)

with open(config_path, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2)
f.write('\n')
return True
except IOError as e:
console.print(f"[red]Error:[/red] Could not write config file: {e}")
return False

@app.command()
def config(
show: bool = typer.Option(False, "--show", "-s", help="Show current configuration"),
get: str = typer.Option(None, "--get", "-g", help="Get a specific configuration value (e.g., 'progress.autoTracking')"),
set_key: str = typer.Option(None, "--set", help="Set a configuration key (e.g., 'progress.autoTracking')"),
set_value: str = typer.Option(None, help="Value for --set (true/false for booleans)"),
auto_tracking: bool = typer.Option(False, "--auto-tracking/--no-auto-tracking", help="Enable or disable auto-tracking"),
project_path: str = typer.Option(None, "--project", "-p", help="Project path (default: current directory)"),
):
"""
Manage Specify project configuration.

View or modify settings for progress tracking and other features.

Examples:
specify config --show # Show current configuration
specify config --get progress.autoTracking # Get specific setting
specify config --auto-tracking true # Enable auto-tracking
specify config --set progress.autoTracking false # Set specific setting
"""
show_banner()

# Determine project path
if project_path:
proj_path = Path(project_path).resolve()
if not proj_path.exists():
console.print(f"[red]Error:[/red] Project path does not exist: {project_path}")
raise typer.Exit(1)
else:
proj_path = Path.cwd()

# Check if this is a Specify project
specify_dir = proj_path / ".specify"
if not specify_dir.exists():
console.print("[yellow]Warning:[/yellow] This doesn't appear to be a Specify project.")
console.print("[dim]Configuration will be created in current directory[/dim]\n")

# Load current config
current_config = _load_config(proj_path)

# Handle different operations
# Check if --auto-tracking or --no-auto-tracking was explicitly used
# Typer sets the flag to True if --auto-tracking, False if --no-auto-tracking
# We need to detect if user actually used the flag (not just default False)
import sys
auto_tracking_used = "--auto-tracking" in sys.argv or "--no-auto-tracking" in sys.argv

if auto_tracking_used:
# Quick set for auto-tracking
current_config["progress"]["autoTracking"] = auto_tracking
if _save_config(current_config, proj_path):
status = "enabled" if auto_tracking else "disabled"
console.print(f"[green]✓[/green] Auto-tracking {status}")
console.print(f"[dim]Configuration saved to: {_get_config_path(proj_path)}[/dim]")
else:
raise typer.Exit(1)
return

if set_key and set_value is not None:
# Set specific key
keys = set_key.split('.')
config_ref = current_config
for key in keys[:-1]:
if key not in config_ref:
config_ref[key] = {}
config_ref = config_ref[key]

# Convert value based on type
final_key = keys[-1]
if isinstance(config_ref.get(final_key), bool):
# Boolean conversion
if set_value.lower() in ('true', '1', 'yes', 'on'):
config_ref[final_key] = True
elif set_value.lower() in ('false', '0', 'no', 'off'):
config_ref[final_key] = False
else:
console.print(f"[red]Error:[/red] Invalid boolean value: {set_value}")
console.print("[dim]Use: true, false, 1, 0, yes, no, on, off[/dim]")
raise typer.Exit(1)
elif isinstance(config_ref.get(final_key), int):
try:
config_ref[final_key] = int(set_value)
except ValueError:
console.print(f"[red]Error:[/red] Invalid integer value: {set_value}")
raise typer.Exit(1)
else:
config_ref[final_key] = set_value

if _save_config(current_config, proj_path):
console.print(f"[green]✓[/green] Set {set_key} = {config_ref[final_key]}")
console.print(f"[dim]Configuration saved to: {_get_config_path(proj_path)}[/dim]")
else:
raise typer.Exit(1)
return

if get:
# Get specific key
keys = get.split('.')
value = current_config
try:
for key in keys:
value = value[key]
console.print(f"[cyan]{get}:[/cyan] {value}")
except KeyError:
console.print(f"[red]Error:[/red] Configuration key not found: {get}")
raise typer.Exit(1)
return

# Default: show all configuration
config_table = Table(show_header=False, box=None, padding=(0, 2))
config_table.add_column("Setting", style="cyan", justify="left")
config_table.add_column("Value", style="white", justify="left")

# Progress settings
config_table.add_row("", "")
config_table.add_row("[bold]Progress Tracking[/bold]", "")
config_table.add_row(" autoTracking", str(current_config["progress"]["autoTracking"]))
config_table.add_row(" updateOnTaskComplete", str(current_config["progress"]["updateOnTaskComplete"]))
config_table.add_row(" updateOnPhaseComplete", str(current_config["progress"]["updateOnPhaseComplete"]))

# Version
config_table.add_row("", "")
config_table.add_row("[bold]Version[/bold]", current_config.get("version", "1.0"))

config_panel = Panel(
config_table,
title="[bold cyan]Specify Configuration[/bold cyan]",
border_style="cyan",
padding=(1, 2)
)

console.print(config_panel)
console.print()

# Show file location
config_file = _get_config_path(proj_path)
if config_file.exists():
console.print(f"[dim]Configuration file: {config_file}[/dim]")
else:
console.print(f"[dim]Configuration file: {config_file} (using defaults)[/dim]")

console.print()
console.print("[bold]Usage examples:[/bold]")
console.print(" [cyan]specify config --auto-tracking[/cyan] # Enable auto-tracking")
console.print(" [cyan]specify config --no-auto-tracking[/cyan] # Disable auto-tracking")
console.print(" [cyan]specify config --get progress.autoTracking[/cyan] # Get specific setting")
console.print(" [cyan]specify config --set progress.autoTracking true[/cyan] # Set specific setting")
console.print()

def main():
app()

Expand Down
6 changes: 5 additions & 1 deletion templates/commands/implement.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,15 @@ You **MUST** consider the user input before proceeding (if not empty).

8. Progress tracking and error handling:
- Report progress after each completed task
- **IMPORTANT** For completed tasks, make sure to mark the task off as [X] in the tasks file.
- **Auto-tracking (optional)**: Check `.specify/config.json` for `progress.autoTracking` setting:
- If `autoTracking: true`: After each completed task, update PROGRESS.md and STATUS.md files using the same logic as `/speckit.progress` command
- If `autoTracking: false` or config missing: Skip automatic progress file updates (user can run `/speckit.progress` manually)
- Progress files location: FEATURE_DIR/PROGRESS.md and FEATURE_DIR/STATUS.md
- Halt execution if any non-parallel task fails
- For parallel tasks [P], continue with successful tasks, report failed ones
- Provide clear error messages with context for debugging
- Suggest next steps if implementation cannot proceed
- **IMPORTANT** For completed tasks, make sure to mark the task off as [X] in the tasks file.

9. Completion validation:
- Verify all required tasks are completed
Expand Down
131 changes: 131 additions & 0 deletions templates/commands/progress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
---
description: Generate and update progress tracking files (PROGRESS.md and STATUS.md) from tasks.md
scripts:
sh: scripts/bash/check-prerequisites.sh --json --require-tasks
ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks
---

## User Input

```text
$ARGUMENTS
```

You **MUST** consider the user input before proceeding (if not empty).

## Outline

1. Run `{SCRIPT}` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").

2. **Load configuration** (if `.specify/config.json` exists):
- Read `.specify/config.json` to check progress tracking settings
- If file doesn't exist, use default settings (autoTracking: false)
- Extract `progress.autoTracking`, `progress.updateOnTaskComplete`, `progress.updateOnPhaseComplete` values

3. **Load tasks.md**:
- **REQUIRED**: Read FEATURE_DIR/tasks.md for the complete task list
- Parse all tasks with format: `- [ ]` or `- [X]` or `- [x]` followed by task ID, labels, and description
- Extract phase information from section headers (e.g., "## Phase 1: Setup", "## Phase 2: Foundational", "## Phase 3: User Story 1")
- Count completed vs total tasks per phase

4. **Calculate progress metrics**:
- **Total tasks**: Count all lines matching task format
- **Completed tasks**: Count lines with `- [X]` or `- [x]`
- **Remaining tasks**: Total - Completed
- **Overall percentage**: (Completed / Total) * 100
- **Per-phase metrics**: Calculate completed/total/percentage for each phase
- **Current phase**: Identify the phase with the most recent activity (last completed task)

5. **Generate PROGRESS.md**:
- Use `.specify/templates/progress-template.md` as structure
- Replace placeholders:
- `[FEATURE_NAME]`: Extract from tasks.md header or FEATURE_DIR name
- `[TIMESTAMP]`: Current date/time in ISO format
- `[TOTAL]`: Total task count
- `[COMPLETED]`: Completed task count
- `[REMAINING]`: Remaining task count
- `[PERCENTAGE]`: Overall progress percentage (rounded to 1 decimal)
- `[PHASE_STATUS_LIST]`: Generate list of phases with status indicators:
- ✅ Complete: Phase with 100% completion
- 🟡 In Progress: Phase with some tasks completed but not all
- ⏳ Pending: Phase with 0% completion
- Format: `- [STATUS] Phase N: [Name] ([completed]/[total] - [percentage]%)`
- `[RECENT_ACTIVITY_LIST]`: List last 5-10 completed tasks (if any) with timestamps
- Write to FEATURE_DIR/PROGRESS.md
- If file exists, update it (preserve any manual notes if present)

6. **Generate STATUS.md** (quick summary):
- Create a concise status file with:
- Current phase name and progress
- Overall progress percentage
- Next steps (next incomplete task or phase)
- Last update timestamp
- Write to FEATURE_DIR/STATUS.md

7. **Display summary**:
- Show progress summary in console:
- Overall: X/Y tasks completed (Z%)
- Current phase: [Phase Name] - X/Y tasks (Z%)
- Next: [Next task or phase]
- Display file paths to PROGRESS.md and STATUS.md

## Progress Calculation Rules

- **Task format detection**: Match lines starting with `- [ ]`, `- [X]`, or `- [x]` followed by task ID (T###)
- **Phase detection**: Match markdown headers `## Phase N:` or `## Phase N: [Name]`
- **Task assignment**: Assign tasks to the phase they appear under (until next phase header)
- **Completion status**:
- `- [ ]` = incomplete
- `- [X]` or `- [x]` = complete
- **Percentage calculation**: Round to 1 decimal place (e.g., 27.3%)

## File Locations

- **PROGRESS.md**: FEATURE_DIR/PROGRESS.md
- **STATUS.md**: FEATURE_DIR/STATUS.md
- **Config**: REPO_ROOT/.specify/config.json (optional)

## Error Handling

- If tasks.md is missing: Report error and suggest running `/speckit.tasks` first
- If no tasks found: Report "No tasks found in tasks.md"
- If config.json is malformed: Use default settings and continue
- If template is missing: Use inline template structure

## Example Output

**PROGRESS.md**:
```markdown
# Progress Tracking: User Authentication

**Last Updated**: 2025-01-27T14:30:00Z
**Status**: 🟢 Active Development

## Overall Progress
- **Total Tasks**: 95
- **Completed**: 26
- **Remaining**: 69
- **Progress**: 27.4%

## Phase Status
- ✅ Phase 1: Setup (5/5 - 100.0%)
- ✅ Phase 2: Foundational (8/8 - 100.0%)
- 🟡 Phase 3: User Story 1 (13/16 - 81.3%)
- ⏳ Phase 4: User Story 2 (0/19 - 0.0%)
```

**STATUS.md**:
```markdown
# Implementation Status

**Current Phase**: Phase 3: User Story 1
**Overall Progress**: 27.4% (26/95 tasks)
**Phase Progress**: 81.3% (13/16 tasks)

**Next Steps**: Complete remaining 3 tasks in Phase 3

**Last Updated**: 2025-01-27T14:30:00Z
```

Note: This command can be run at any time to refresh progress tracking files, regardless of whether implementation is active.

9 changes: 9 additions & 0 deletions templates/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"progress": {
"autoTracking": false,
"updateOnTaskComplete": true,
"updateOnPhaseComplete": true
},
"version": "1.0"
}

Loading