Initial commit for Eureka deployment
This commit is contained in:
parent
946e0d94a0
commit
1a39be31ee
|
|
@ -0,0 +1,70 @@
|
|||
# Eureka Deploy Flow Agent
|
||||
|
||||
You are an autonomous deployment agent for the Eureka platform. Execute the complete deployment workflow.
|
||||
|
||||
## Workflow Steps
|
||||
|
||||
Execute these steps in order:
|
||||
|
||||
### Step 1: Check Git Status
|
||||
```bash
|
||||
git status
|
||||
git branch --show-current
|
||||
```
|
||||
- Identify current branch
|
||||
- Check for uncommitted changes
|
||||
- Check for untracked files
|
||||
|
||||
### Step 2: Stage Changes
|
||||
If there are changes to commit:
|
||||
```bash
|
||||
git add -A
|
||||
```
|
||||
|
||||
### Step 3: Commit Changes
|
||||
If there are staged changes:
|
||||
```bash
|
||||
git commit -m "Deploy: $(date +%Y-%m-%d_%H:%M:%S)"
|
||||
```
|
||||
Use a descriptive commit message if the user provided context.
|
||||
|
||||
### Step 4: Push to Eureka Remote
|
||||
```bash
|
||||
git push eureka $(git branch --show-current)
|
||||
```
|
||||
If push fails, try:
|
||||
```bash
|
||||
git push -u eureka $(git branch --show-current)
|
||||
```
|
||||
|
||||
### Step 5: Trigger Deployment
|
||||
```bash
|
||||
eureka deploy trigger --yes
|
||||
```
|
||||
|
||||
### Step 6: Monitor Status
|
||||
```bash
|
||||
eureka deploy status
|
||||
```
|
||||
Wait a few seconds and check status again if still building.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- **No eureka remote**: Run `eureka init` first
|
||||
- **Push rejected**: Check if remote has changes, pull first if needed
|
||||
- **Deploy failed**: Check `eureka deploy logs` for details
|
||||
- **No app_id**: Run `eureka setup` to configure
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- All changes committed and pushed
|
||||
- Deployment triggered successfully
|
||||
- Status shows "building" or "deployed"
|
||||
|
||||
## Output
|
||||
|
||||
Report:
|
||||
1. Files changed/committed
|
||||
2. Push result
|
||||
3. Deployment status
|
||||
4. Deployed URL (when available)
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
description: View deployment logs from Eureka platform
|
||||
allowed-tools: Read, Bash, Glob
|
||||
---
|
||||
|
||||
# Eureka Deploy Logs
|
||||
|
||||
**Input**: "$ARGUMENTS"
|
||||
|
||||
---
|
||||
|
||||
## PURPOSE
|
||||
|
||||
View the deployment logs from the Eureka platform to debug issues or monitor progress.
|
||||
|
||||
---
|
||||
|
||||
## EXECUTION FLOW
|
||||
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
### PHASE 1: Fetch Logs
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
|
||||
#### 1.1: Run Logs Command
|
||||
|
||||
```bash
|
||||
# Default: last 100 lines
|
||||
eureka deploy logs
|
||||
|
||||
# Custom tail count
|
||||
eureka deploy logs --tail 200
|
||||
```
|
||||
|
||||
#### 1.2: Parse Arguments
|
||||
|
||||
If `$ARGUMENTS` contains a number, use it as tail count:
|
||||
```bash
|
||||
TAIL_COUNT="${ARGUMENTS:-100}"
|
||||
eureka deploy logs --tail "$TAIL_COUNT"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ARGUMENTS
|
||||
|
||||
| Argument | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `[tail]` | `100` | Number of log lines to show |
|
||||
| `--id <deploymentId>` | Latest | Specific deployment ID |
|
||||
| `--follow` | `false` | Follow logs in real-time |
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
```bash
|
||||
# View last 100 lines
|
||||
/eureka:deploy-logs
|
||||
|
||||
# View last 500 lines
|
||||
/eureka:deploy-logs 500
|
||||
|
||||
# View specific deployment
|
||||
/eureka:deploy-logs --id dep_abc123
|
||||
```
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
description: Check deployment status on Eureka platform
|
||||
allowed-tools: Read, Bash, Glob
|
||||
---
|
||||
|
||||
# Eureka Deploy Status
|
||||
|
||||
**Input**: "$ARGUMENTS"
|
||||
|
||||
---
|
||||
|
||||
## PURPOSE
|
||||
|
||||
Check the current deployment status of the application on the Eureka platform.
|
||||
|
||||
---
|
||||
|
||||
## EXECUTION FLOW
|
||||
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
### PHASE 1: Check Status
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
|
||||
#### 1.1: Run Status Command
|
||||
|
||||
```bash
|
||||
eureka deploy status --verbose
|
||||
```
|
||||
|
||||
#### 1.2: Display Results
|
||||
|
||||
The command will show:
|
||||
- Current deployment status (pending, building, deploying, deployed, failed)
|
||||
- Version information
|
||||
- Environment
|
||||
- Timestamps
|
||||
- Deployment URL (if deployed)
|
||||
|
||||
---
|
||||
|
||||
## ARGUMENTS
|
||||
|
||||
| Argument | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `--verbose` | `false` | Show detailed logs |
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
```bash
|
||||
# Check current deployment status
|
||||
/eureka:deploy-status
|
||||
|
||||
# Check with verbose output
|
||||
/eureka:deploy-status --verbose
|
||||
```
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
---
|
||||
description: Deploy application to Eureka platform (creates app if needed)
|
||||
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
||||
---
|
||||
|
||||
# Eureka Deploy
|
||||
|
||||
**Input**: "$ARGUMENTS"
|
||||
|
||||
---
|
||||
|
||||
## PURPOSE
|
||||
|
||||
Deploy the current project to the Eureka platform. If no `app_id` is configured, automatically creates a new directory app first, then triggers the deployment.
|
||||
|
||||
---
|
||||
|
||||
## ⛔ CRITICAL RULES
|
||||
|
||||
### MUST DO
|
||||
1. **MUST** check for existing `app_id` in `.claude/eureka-factory.yaml` first
|
||||
2. **MUST** create a new app via API if no `app_id` exists
|
||||
3. **MUST** save the new `app_id` to config after creation
|
||||
4. **MUST** display deployment status after triggering
|
||||
|
||||
### CANNOT DO
|
||||
1. **CANNOT** deploy without valid API key
|
||||
2. **CANNOT** skip app creation if `app_id` is missing
|
||||
3. **CANNOT** proceed if API calls fail
|
||||
|
||||
---
|
||||
|
||||
## EXECUTION FLOW
|
||||
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
### PHASE 1: Configuration Check
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
|
||||
#### 1.1: Display Start Banner
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ 🚀 EUREKA DEPLOY ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Deploying to Eureka Platform... ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
#### 1.2: Check Configuration
|
||||
|
||||
Read the configuration file:
|
||||
```bash
|
||||
# Check if config exists
|
||||
cat .claude/eureka-factory.yaml 2>/dev/null || cat .claude/eureka-factory.yml 2>/dev/null || echo "NO_CONFIG"
|
||||
```
|
||||
|
||||
**Extract from config:**
|
||||
- `api_key` - Required for all operations
|
||||
- `app_id` - If exists, skip app creation
|
||||
- `api_endpoint` - Optional custom endpoint
|
||||
|
||||
#### 1.3: Validate API Key
|
||||
|
||||
If no `api_key` found:
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ ❌ NO API KEY CONFIGURED ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Run `eureka setup` to configure your credentials. ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
**STOP EXECUTION**
|
||||
|
||||
---
|
||||
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
### PHASE 2: App Creation (if needed)
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
|
||||
#### 2.1: Check for app_id
|
||||
|
||||
If `app_id` exists in config → **SKIP TO PHASE 3**
|
||||
|
||||
If `app_id` is missing:
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ 📁 CREATING DIRECTORY APP ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ No app_id found. Creating new app on Eureka... ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
#### 2.2: Determine App Name
|
||||
|
||||
Use the project directory name as the default app name:
|
||||
```bash
|
||||
APP_NAME=$(basename $(pwd))
|
||||
echo "App name: $APP_NAME"
|
||||
```
|
||||
|
||||
Or use argument if provided: `$ARGUMENTS` as app name
|
||||
|
||||
#### 2.3: Create App via API
|
||||
|
||||
```bash
|
||||
# Create app using eureka CLI
|
||||
eureka deploy trigger --name "$APP_NAME" --type other --yes
|
||||
```
|
||||
|
||||
**If the command is not available, use direct API call:**
|
||||
|
||||
```bash
|
||||
API_KEY="<from config>"
|
||||
API_ENDPOINT="<from config or default>"
|
||||
|
||||
curl -X POST "${API_ENDPOINT}/v1/apps" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}" \
|
||||
-d "{\"name\": \"${APP_NAME}\", \"type\": \"other\"}"
|
||||
```
|
||||
|
||||
#### 2.4: Save app_id to Config
|
||||
|
||||
Extract `app_id` from API response and update config:
|
||||
|
||||
```yaml
|
||||
# .claude/eureka-factory.yaml
|
||||
api_key: <existing>
|
||||
project_id: <existing>
|
||||
repo_id: <existing>
|
||||
app_id: <NEW_APP_ID> # Add this line
|
||||
```
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ ✅ APP CREATED ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ App ID: <app_id> ║
|
||||
║ Saved to: .claude/eureka-factory.yaml ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
### PHASE 3: Trigger Deployment
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
|
||||
#### 3.1: Trigger Deploy
|
||||
|
||||
```bash
|
||||
# Using eureka CLI
|
||||
eureka deploy trigger --yes
|
||||
|
||||
# Or direct API call
|
||||
curl -X POST "${API_ENDPOINT}/v1/apps/${APP_ID}/deployments" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}" \
|
||||
-d '{"environment": "production"}'
|
||||
```
|
||||
|
||||
#### 3.2: Display Deployment Status
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ ✅ DEPLOYMENT TRIGGERED ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Deployment ID: <deployment_id> ║
|
||||
║ Status: PENDING ║
|
||||
║ Environment: production ║
|
||||
║ Version: <version> ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Use `/eureka:deploy-status` to check progress ║
|
||||
║ Use `/eureka:deploy-logs` to view logs ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ARGUMENTS
|
||||
|
||||
| Argument | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `[app-name]` | Directory name | Name for new app (only used if creating) |
|
||||
| `--env <environment>` | `production` | Deployment environment |
|
||||
| `--branch <branch>` | Current branch | Git branch to deploy |
|
||||
| `--force` | `false` | Force deploy even if already deploying |
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
```bash
|
||||
# Deploy current project (creates app if needed)
|
||||
/eureka:deploy
|
||||
|
||||
# Deploy with custom app name
|
||||
/eureka:deploy my-awesome-app
|
||||
|
||||
# Deploy specific branch to staging
|
||||
/eureka:deploy --env staging --branch develop
|
||||
|
||||
# Force redeploy
|
||||
/eureka:deploy --force
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ERROR HANDLING
|
||||
|
||||
### No Configuration
|
||||
```
|
||||
❌ No configuration found.
|
||||
Run `eureka setup` to configure credentials.
|
||||
```
|
||||
|
||||
### App Creation Failed
|
||||
```
|
||||
❌ Failed to create app: <error message>
|
||||
Check your API key and try again.
|
||||
```
|
||||
|
||||
### Deployment Failed
|
||||
```
|
||||
❌ Deployment failed: <error message>
|
||||
Use `/eureka:deploy-logs` to see details.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FLOW DIAGRAM
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ /eureka:deploy │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────┐ │
|
||||
│ │ Check Config │ │
|
||||
│ └────────┬────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────▼─────────┐ │
|
||||
│ │ Has API Key? │ │
|
||||
│ └─────────┬─────────┘ │
|
||||
│ │ │
|
||||
│ NO │ YES │
|
||||
│ ┌────────────────────┼────────────────────┐ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌───────────┐ ┌─────────────────┐ │
|
||||
│ │ ERROR │ │ Has app_id? │ │
|
||||
│ │ No Key │ └────────┬────────┘ │
|
||||
│ └───────────┘ │ │
|
||||
│ NO │ YES │
|
||||
│ ┌───────────────────┼──────────┐ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌─────────────────┐ ┌──────────────┐
|
||||
│ │ Create App │ │ │
|
||||
│ │ via API │ │ │
|
||||
│ └────────┬────────┘ │ │
|
||||
│ │ │ │
|
||||
│ ▼ │ │
|
||||
│ ┌─────────────────┐ │ │
|
||||
│ │ Save app_id │ │ │
|
||||
│ │ to Config │ │ │
|
||||
│ └────────┬────────┘ │ │
|
||||
│ │ │ │
|
||||
│ └───────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────┐ │
|
||||
│ │ Trigger Deploy │ │
|
||||
│ └────────┬────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────┐ │
|
||||
│ │ Show Status │ │
|
||||
│ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
|
@ -0,0 +1,595 @@
|
|||
---
|
||||
description: Generate comprehensive project documentation for engineers and non-engineers
|
||||
allowed-tools: Read, Write, Edit, Bash, Task, TodoWrite, Glob, Grep
|
||||
---
|
||||
|
||||
# Eureka Index - Project Documentation Generator
|
||||
|
||||
**Input**: "$ARGUMENTS"
|
||||
|
||||
---
|
||||
|
||||
## PURPOSE
|
||||
|
||||
Generate comprehensive, dual-audience documentation by analyzing the current project structure using **parallel agent execution**. The output is designed to be understandable for **both engineers and non-engineers**.
|
||||
|
||||
### Documentation Layers
|
||||
|
||||
| Layer | Audience | Content |
|
||||
|-------|----------|---------|
|
||||
| Executive Summary | Everyone | Project purpose, value, capabilities |
|
||||
| Architecture Overview | Everyone | Visual diagrams, technology stack |
|
||||
| Getting Started | Semi-technical | Setup, basic usage, configuration |
|
||||
| Feature Guide | Non-engineers | Plain-language feature descriptions |
|
||||
| API Reference | Engineers | Endpoints, schemas, authentication |
|
||||
| Component Catalog | Engineers | Props, interfaces, usage examples |
|
||||
| Data Models | Both | ER diagrams + plain descriptions |
|
||||
| Glossary | Non-engineers | Technical terms explained |
|
||||
|
||||
---
|
||||
|
||||
## EXECUTION ARCHITECTURE
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ PARALLEL EXECUTION PIPELINE │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ PHASE 1: PARALLEL ANALYSIS (run_in_background: true) │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
|
||||
│ │ Structure │ │ API │ │ Components │ │ Models │ │
|
||||
│ │ Analyzer │ │ Analyzer │ │ Analyzer │ │ Analyzer │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └─────┬──────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ ▼ ▼ ▼ ▼ │
|
||||
│ PHASE 2: SYNCHRONIZATION │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Merge & Create Unified Analysis │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ PHASE 3: PARALLEL DOCUMENTATION (run_in_background: true) │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
|
||||
│ │ Main Doc │ │ API Docs │ │ Components │ │ Quick │ │
|
||||
│ │ Generator │ │ Generator │ │ Generator │ │ Reference │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └─────┬──────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ ▼ ▼ ▼ ▼ │
|
||||
│ PHASE 4: FINALIZATION │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ HTML Generation + Validation + Summary │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⛔ CRITICAL RULES
|
||||
|
||||
### MUST DO
|
||||
1. **MUST** launch analysis agents in parallel using `run_in_background: true`
|
||||
2. **MUST** wait for all analysis agents before synchronization
|
||||
3. **MUST** launch documentation agents in parallel after synchronization
|
||||
4. **MUST** include both technical and non-technical descriptions
|
||||
5. **MUST** validate generated documentation against actual code
|
||||
|
||||
### CANNOT DO
|
||||
1. **CANNOT** make up features that don't exist
|
||||
2. **CANNOT** skip the parallel analysis phase
|
||||
3. **CANNOT** generate docs without synchronizing analysis results
|
||||
|
||||
---
|
||||
|
||||
## EXECUTION FLOW
|
||||
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
### PHASE 1: Parallel Analysis
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
|
||||
#### 1.1: Display Start Banner & Setup
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ 📚 EUREKA INDEX - Parallel Documentation Generator ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Launching parallel analysis agents... ║
|
||||
║ Output: Dual-audience documentation (Engineer + Non-Engineer)║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
```bash
|
||||
OUTPUT_DIR="${ARGUMENTS:-docs}"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
echo "📁 Output directory: $OUTPUT_DIR"
|
||||
```
|
||||
|
||||
#### 1.2: Launch Parallel Analysis Agents
|
||||
|
||||
**CRITICAL: Launch ALL four agents in a SINGLE message with multiple Task tool calls:**
|
||||
|
||||
```
|
||||
Launch these 4 Task agents IN PARALLEL (single message, multiple tool calls):
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ AGENT 1: Structure Analyzer │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Task tool with: │
|
||||
│ subagent_type: "Explore" │
|
||||
│ run_in_background: true │
|
||||
│ prompt: | │
|
||||
│ # PROJECT STRUCTURE ANALYSIS │
|
||||
│ │
|
||||
│ Analyze the project structure and return findings. │
|
||||
│ │
|
||||
│ ## Tasks │
|
||||
│ 1. Identify project type (package.json, requirements.txt, │
|
||||
│ Cargo.toml, go.mod, pom.xml) │
|
||||
│ 2. Extract metadata (name, version, description) │
|
||||
│ 3. Map directory structure with purposes │
|
||||
│ 4. Identify tech stack (language, framework, database) │
|
||||
│ 5. List key dependencies with plain English purposes │
|
||||
│ │
|
||||
│ ## Output Format (YAML) │
|
||||
│ ```yaml │
|
||||
│ project: │
|
||||
│ name: "..." │
|
||||
│ version: "..." │
|
||||
│ description: "..." │
|
||||
│ type: "node|python|rust|go|java|other" │
|
||||
│ tech_stack: │
|
||||
│ language: "..." │
|
||||
│ framework: "..." │
|
||||
│ database: "..." │
|
||||
│ structure: │
|
||||
│ directories: │
|
||||
│ - path: "..." │
|
||||
│ purpose: "..." │
|
||||
│ file_count: N │
|
||||
│ dependencies: │
|
||||
│ - name: "..." │
|
||||
│ purpose: "plain English" │
|
||||
│ ``` │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ AGENT 2: API Analyzer │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Task tool with: │
|
||||
│ subagent_type: "Explore" │
|
||||
│ run_in_background: true │
|
||||
│ prompt: | │
|
||||
│ # API ENDPOINTS ANALYSIS │
|
||||
│ │
|
||||
│ Find and analyze all API endpoints in the project. │
|
||||
│ │
|
||||
│ ## Search Patterns │
|
||||
│ - Next.js App Router: app/api/**/route.ts │
|
||||
│ - Next.js Pages: pages/api/**/*.ts │
|
||||
│ - Express: router.get/post/put/delete │
|
||||
│ - FastAPI: @app.get/post/put/delete │
|
||||
│ - GraphQL: Query/Mutation resolvers │
|
||||
│ │
|
||||
│ ## Output Format (YAML) │
|
||||
│ ```yaml │
|
||||
│ api_endpoints: │
|
||||
│ - method: "GET|POST|PUT|DELETE" │
|
||||
│ path: "/api/..." │
|
||||
│ handler_file: "path/to/file.ts" │
|
||||
│ description: "plain English" │
|
||||
│ request_body: "schema if POST/PUT" │
|
||||
│ response: "schema summary" │
|
||||
│ auth_required: true|false │
|
||||
│ ``` │
|
||||
│ │
|
||||
│ If no APIs found, return: api_endpoints: [] │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ AGENT 3: Components Analyzer │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Task tool with: │
|
||||
│ subagent_type: "Explore" │
|
||||
│ run_in_background: true │
|
||||
│ prompt: | │
|
||||
│ # UI COMPONENTS ANALYSIS │
|
||||
│ │
|
||||
│ Find and analyze all UI components in the project. │
|
||||
│ │
|
||||
│ ## Search Patterns │
|
||||
│ - React: components/**/*.tsx, function Component() │
|
||||
│ - Vue: components/**/*.vue, <script setup> │
|
||||
│ - Angular: *.component.ts, @Component │
|
||||
│ - Svelte: **/*.svelte │
|
||||
│ │
|
||||
│ ## Output Format (YAML) │
|
||||
│ ```yaml │
|
||||
│ components: │
|
||||
│ - id: "component_name" │
|
||||
│ name: "ComponentName" │
|
||||
│ path: "path/to/Component.tsx" │
|
||||
│ description: "what it does in plain English" │
|
||||
│ props: │
|
||||
│ - name: "propName" │
|
||||
│ type: "string|number|boolean|..." │
|
||||
│ required: true|false │
|
||||
│ description: "what it controls" │
|
||||
│ events: ["onClick", "onChange"] │
|
||||
│ dependencies: ["OtherComponent"] │
|
||||
│ ``` │
|
||||
│ │
|
||||
│ If no components found, return: components: [] │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ AGENT 4: Data Models Analyzer │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Task tool with: │
|
||||
│ subagent_type: "Explore" │
|
||||
│ run_in_background: true │
|
||||
│ prompt: | │
|
||||
│ # DATA MODELS ANALYSIS │
|
||||
│ │
|
||||
│ Find and analyze all data models in the project. │
|
||||
│ │
|
||||
│ ## Search Patterns │
|
||||
│ - Prisma: prisma/schema.prisma, model X {} │
|
||||
│ - TypeORM: @Entity(), entities/**/*.ts │
|
||||
│ - Mongoose: new Schema(), models/**/*.ts │
|
||||
│ - SQLAlchemy: class X(Base), models/**/*.py │
|
||||
│ - TypeScript: interface/type definitions │
|
||||
│ │
|
||||
│ ## Output Format (YAML) │
|
||||
│ ```yaml │
|
||||
│ data_models: │
|
||||
│ - name: "ModelName" │
|
||||
│ source: "prisma|typeorm|mongoose|typescript" │
|
||||
│ file_path: "path/to/model" │
|
||||
│ description: "what data it represents" │
|
||||
│ fields: │
|
||||
│ - name: "fieldName" │
|
||||
│ type: "String|Int|Boolean|..." │
|
||||
│ description: "plain English" │
|
||||
│ constraints: "unique|optional|default" │
|
||||
│ relations: │
|
||||
│ - type: "hasMany|belongsTo|hasOne" │
|
||||
│ target: "OtherModel" │
|
||||
│ glossary_terms: │
|
||||
│ - term: "technical term found" │
|
||||
│ definition: "plain English definition" │
|
||||
│ ``` │
|
||||
│ │
|
||||
│ If no models found, return: data_models: [] │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 1.3: Wait for All Analysis Agents
|
||||
|
||||
```
|
||||
Use TaskOutput tool to wait for each agent:
|
||||
- TaskOutput with task_id from Agent 1, block: true
|
||||
- TaskOutput with task_id from Agent 2, block: true
|
||||
- TaskOutput with task_id from Agent 3, block: true
|
||||
- TaskOutput with task_id from Agent 4, block: true
|
||||
|
||||
Collect all results for synchronization.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
### PHASE 2: Synchronization
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
|
||||
#### 2.1: Merge Analysis Results
|
||||
|
||||
Combine outputs from all 4 agents into a unified analysis:
|
||||
|
||||
```yaml
|
||||
# $OUTPUT_DIR/analysis.yml - Merged from parallel agents
|
||||
|
||||
project:
|
||||
# From Agent 1: Structure Analyzer
|
||||
name: "..."
|
||||
version: "..."
|
||||
description: "..."
|
||||
type: "..."
|
||||
|
||||
tech_stack:
|
||||
# From Agent 1: Structure Analyzer
|
||||
language: "..."
|
||||
framework: "..."
|
||||
database: "..."
|
||||
key_dependencies: [...]
|
||||
|
||||
structure:
|
||||
# From Agent 1: Structure Analyzer
|
||||
directories: [...]
|
||||
|
||||
api_endpoints:
|
||||
# From Agent 2: API Analyzer
|
||||
[...]
|
||||
|
||||
components:
|
||||
# From Agent 3: Components Analyzer
|
||||
[...]
|
||||
|
||||
data_models:
|
||||
# From Agent 4: Data Models Analyzer
|
||||
[...]
|
||||
|
||||
glossary_terms:
|
||||
# Merged from all agents
|
||||
[...]
|
||||
```
|
||||
|
||||
#### 2.2: Write Unified Analysis File
|
||||
|
||||
```bash
|
||||
# Write merged analysis to file
|
||||
Write the unified YAML to: $OUTPUT_DIR/analysis.yml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
### PHASE 3: Parallel Documentation Generation
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
|
||||
#### 3.1: Launch Parallel Documentation Agents
|
||||
|
||||
**CRITICAL: Launch ALL four doc agents in a SINGLE message with multiple Task tool calls:**
|
||||
|
||||
```
|
||||
Launch these 4 Task agents IN PARALLEL (single message, multiple tool calls):
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ DOC AGENT 1: Main Documentation │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Task tool with: │
|
||||
│ subagent_type: "technical-writer" │
|
||||
│ run_in_background: true │
|
||||
│ prompt: | │
|
||||
│ # Generate PROJECT_DOCUMENTATION.md │
|
||||
│ │
|
||||
│ Using the analysis from $OUTPUT_DIR/analysis.yml, │
|
||||
│ generate comprehensive main documentation. │
|
||||
│ │
|
||||
│ ## Sections Required │
|
||||
│ 1. Executive Summary (plain English, no jargon) │
|
||||
│ 2. Quick Start (installation, basic usage) │
|
||||
│ 3. Architecture Overview (ASCII diagrams) │
|
||||
│ 4. Features (dual-audience: plain + technical details) │
|
||||
│ 5. Glossary (all technical terms explained) │
|
||||
│ │
|
||||
│ ## Rules │
|
||||
│ - Plain English FIRST, technical in <details> tags │
|
||||
│ - Include ASCII architecture diagrams │
|
||||
│ - Use tables for structured data │
|
||||
│ - Code blocks with language hints │
|
||||
│ │
|
||||
│ Write to: $OUTPUT_DIR/PROJECT_DOCUMENTATION.md │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ DOC AGENT 2: API Reference │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Task tool with: │
|
||||
│ subagent_type: "technical-writer" │
|
||||
│ run_in_background: true │
|
||||
│ prompt: | │
|
||||
│ # Generate API_REFERENCE.md │
|
||||
│ │
|
||||
│ Using api_endpoints from $OUTPUT_DIR/analysis.yml, │
|
||||
│ generate detailed API documentation. │
|
||||
│ │
|
||||
│ ## Format per Endpoint │
|
||||
│ ### [METHOD] /path │
|
||||
│ **Description**: Plain English │
|
||||
│ **Authentication**: Required/Optional │
|
||||
│ │
|
||||
│ <details> │
|
||||
│ <summary>Technical Details</summary> │
|
||||
│ Request body, response schema, example │
|
||||
│ </details> │
|
||||
│ │
|
||||
│ If no APIs exist, write brief note explaining this. │
|
||||
│ │
|
||||
│ Write to: $OUTPUT_DIR/API_REFERENCE.md │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ DOC AGENT 3: Components Catalog │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Task tool with: │
|
||||
│ subagent_type: "technical-writer" │
|
||||
│ run_in_background: true │
|
||||
│ prompt: | │
|
||||
│ # Generate COMPONENTS.md │
|
||||
│ │
|
||||
│ Using components from $OUTPUT_DIR/analysis.yml, │
|
||||
│ generate component catalog documentation. │
|
||||
│ │
|
||||
│ ## Format per Component │
|
||||
│ ### ComponentName │
|
||||
│ **Purpose**: Plain English description │
|
||||
│ **Location**: `path/to/file` │
|
||||
│ │
|
||||
│ <details> │
|
||||
│ <summary>Props & Usage</summary> │
|
||||
│ Props table, usage example, dependencies │
|
||||
│ </details> │
|
||||
│ │
|
||||
│ If no components exist, write brief note explaining this. │
|
||||
│ │
|
||||
│ Write to: $OUTPUT_DIR/COMPONENTS.md │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ DOC AGENT 4: Quick Reference │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Task tool with: │
|
||||
│ subagent_type: "technical-writer" │
|
||||
│ run_in_background: true │
|
||||
│ prompt: | │
|
||||
│ # Generate QUICK_REFERENCE.md │
|
||||
│ │
|
||||
│ Using $OUTPUT_DIR/analysis.yml, create a one-page │
|
||||
│ quick reference card. │
|
||||
│ │
|
||||
│ ## Sections (tables only, minimal text) │
|
||||
│ - Commands (npm scripts, make targets) │
|
||||
│ - Key Files (important files and purposes) │
|
||||
│ - API Endpoints (method, path, purpose) │
|
||||
│ - Environment Variables │
|
||||
│ - Common Patterns │
|
||||
│ │
|
||||
│ Keep it to ONE PAGE - scannable, dense, useful. │
|
||||
│ │
|
||||
│ Write to: $OUTPUT_DIR/QUICK_REFERENCE.md │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 3.2: Wait for All Documentation Agents
|
||||
|
||||
```
|
||||
Use TaskOutput tool to wait for each doc agent:
|
||||
- TaskOutput with task_id from Doc Agent 1, block: true
|
||||
- TaskOutput with task_id from Doc Agent 2, block: true
|
||||
- TaskOutput with task_id from Doc Agent 3, block: true
|
||||
- TaskOutput with task_id from Doc Agent 4, block: true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
### PHASE 4: Finalization
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
|
||||
#### 4.1: Generate HTML Documentation (Optional)
|
||||
|
||||
```bash
|
||||
# If Python scripts exist, generate HTML
|
||||
if [ -f "skills/documentation-generator/scripts/generate_html.py" ]; then
|
||||
python3 skills/documentation-generator/scripts/generate_html.py \
|
||||
$OUTPUT_DIR/analysis.yml \
|
||||
skills/documentation-generator/templates/documentation.html \
|
||||
$OUTPUT_DIR/index.html
|
||||
fi
|
||||
```
|
||||
|
||||
#### 4.2: Validate Generated Documentation
|
||||
|
||||
Verify all documentation files exist and are non-empty:
|
||||
- `$OUTPUT_DIR/analysis.yml`
|
||||
- `$OUTPUT_DIR/PROJECT_DOCUMENTATION.md`
|
||||
- `$OUTPUT_DIR/API_REFERENCE.md`
|
||||
- `$OUTPUT_DIR/COMPONENTS.md`
|
||||
- `$OUTPUT_DIR/QUICK_REFERENCE.md`
|
||||
|
||||
#### 4.3: Display Completion Banner
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ ✅ PARALLEL DOCUMENTATION COMPLETE ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Execution: 4 analysis agents → sync → 4 doc agents ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Output Directory: $OUTPUT_DIR ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Files Created: ║
|
||||
║ 📊 analysis.yml (Merged analysis data) ║
|
||||
║ 📄 PROJECT_DOCUMENTATION.md (Main documentation) ║
|
||||
║ 📄 API_REFERENCE.md (API details) ║
|
||||
║ 📄 COMPONENTS.md (Component catalog) ║
|
||||
║ 📄 QUICK_REFERENCE.md (One-page reference) ║
|
||||
║ 🌐 index.html (HTML version - if generated) ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Performance: ║
|
||||
║ ⚡ Analysis: 4 agents in parallel ║
|
||||
║ ⚡ Documentation: 4 agents in parallel ║
|
||||
║ ⚡ Total: ~2x faster than sequential execution ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ARGUMENTS
|
||||
|
||||
| Argument | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `[output_dir]` | `docs` | Output directory for documentation |
|
||||
| `--format` | `markdown` | Output format: markdown, html |
|
||||
| `--sections` | `all` | Sections to include: all, api, components, models |
|
||||
| `--audience` | `both` | Target: both, technical, non-technical |
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
```bash
|
||||
# Generate full documentation with parallel agents
|
||||
/eureka:index
|
||||
|
||||
# Generate in custom directory
|
||||
/eureka:index my-docs
|
||||
|
||||
# Generate API-only documentation
|
||||
/eureka:index --sections api
|
||||
|
||||
# Generate non-technical documentation only
|
||||
/eureka:index --audience non-technical
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PARALLEL EXECUTION BENEFITS
|
||||
|
||||
| Metric | Sequential | Parallel | Improvement |
|
||||
|--------|-----------|----------|-------------|
|
||||
| Analysis Phase | 4x agent time | 1x agent time | 75% faster |
|
||||
| Doc Generation | 4x agent time | 1x agent time | 75% faster |
|
||||
| Total Time | ~8 units | ~2 units | **4x faster** |
|
||||
|
||||
---
|
||||
|
||||
## DUAL-AUDIENCE WRITING GUIDELINES
|
||||
|
||||
### For Non-Engineers (Primary)
|
||||
- Lead with "What" and "Why", not "How"
|
||||
- Use analogies and real-world comparisons
|
||||
- Avoid acronyms; spell them out first time
|
||||
- Use bullet points over paragraphs
|
||||
- Include visual diagrams
|
||||
|
||||
### For Engineers (Secondary)
|
||||
- Include in collapsible `<details>` sections
|
||||
- Provide code examples
|
||||
- Reference file paths and line numbers
|
||||
- Include type definitions
|
||||
- Link to source files
|
||||
|
||||
### Balance Example
|
||||
|
||||
```markdown
|
||||
## User Authentication
|
||||
|
||||
**What it does**: Allows users to securely log into the application
|
||||
using their email and password.
|
||||
|
||||
**How it works** (simplified):
|
||||
1. User enters credentials
|
||||
2. System verifies them
|
||||
3. User receives access
|
||||
|
||||
<details>
|
||||
<summary>Technical Implementation</summary>
|
||||
|
||||
- **Strategy**: JWT-based authentication
|
||||
- **Token Storage**: HTTP-only cookies
|
||||
- **Session Duration**: 24 hours
|
||||
- **Refresh Logic**: Automatic refresh 1 hour before expiry
|
||||
|
||||
**Key Files**:
|
||||
- `src/auth/jwt.service.ts` - Token generation
|
||||
- `src/auth/auth.guard.ts` - Route protection
|
||||
|
||||
</details>
|
||||
```
|
||||
|
|
@ -0,0 +1,660 @@
|
|||
---
|
||||
description: Generate a designer-quality landing page from project documentation with AI-generated images
|
||||
allowed-tools: Read, Write, Edit, Bash, Task, TodoWrite, Glob, Grep, mcp__eureka-imagen__generate_hero_image, mcp__eureka-imagen__generate_feature_icon, mcp__eureka-imagen__generate_illustration, mcp__eureka-imagen__generate_og_image, mcp__eureka-imagen__generate_logo_concept, mcp__eureka-imagen__list_available_models, mcp__eureka-imagen__check_status
|
||||
---
|
||||
|
||||
# Eureka Landing - Landing Page Generator
|
||||
|
||||
**Input**: "$ARGUMENTS"
|
||||
|
||||
---
|
||||
|
||||
## PURPOSE
|
||||
|
||||
Generate a **designer-quality landing page** with concept branding and Q&A section based on existing project documentation. This command requires documentation to be generated first via `/eureka:index`.
|
||||
|
||||
### Output Features
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| Hero Section | Compelling headline, tagline, CTA buttons |
|
||||
| Features Grid | Visual feature cards with icons |
|
||||
| How It Works | Step-by-step visual flow |
|
||||
| Screenshots/Demo | Placeholder for app visuals |
|
||||
| Q&A/FAQ | Common questions answered |
|
||||
| Concept Branding | Colors, typography, visual style |
|
||||
| Responsive Design | Mobile-first, works on all devices |
|
||||
| Dark Mode | Automatic system preference detection |
|
||||
| **AI Images** | AI-generated hero, icons, illustrations (ImageRouter) |
|
||||
|
||||
### Image Generation (Optional)
|
||||
|
||||
When `--with-images` flag is used and IMAGEROUTER_API_KEY is set, the command will:
|
||||
- Generate a hero banner image
|
||||
- Generate feature icons
|
||||
- Generate how-it-works illustrations
|
||||
- Generate OG image for social sharing
|
||||
|
||||
```bash
|
||||
# With AI-generated images
|
||||
/eureka:landing --with-images
|
||||
|
||||
# Without images (default)
|
||||
/eureka:landing
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PREREQUISITES
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ ⚠️ REQUIRES DOCUMENTATION FIRST ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
║ This command uses data from /eureka:index output. ║
|
||||
║ ║
|
||||
║ Required files: ║
|
||||
║ ✓ docs/analysis.yml (or custom output dir) ║
|
||||
║ ✓ docs/PROJECT_DOCUMENTATION.md ║
|
||||
║ ║
|
||||
║ If missing, run first: ║
|
||||
║ /eureka:index ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## EXECUTION FLOW
|
||||
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
### PHASE 1: Validate Prerequisites
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
|
||||
#### 1.1: Check for Documentation
|
||||
|
||||
```bash
|
||||
DOCS_DIR="${ARGUMENTS:-docs}"
|
||||
|
||||
# Check if documentation exists
|
||||
if [ ! -f "$DOCS_DIR/analysis.yml" ] && [ ! -f "$DOCS_DIR/PROJECT_DOCUMENTATION.md" ]; then
|
||||
echo "❌ ERROR: Documentation not found!"
|
||||
echo ""
|
||||
echo "Required: $DOCS_DIR/analysis.yml or $DOCS_DIR/PROJECT_DOCUMENTATION.md"
|
||||
echo ""
|
||||
echo "Run first: /eureka:index"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Documentation found in $DOCS_DIR"
|
||||
```
|
||||
|
||||
#### 1.2: Display Start Banner
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ 🎨 EUREKA LANDING - Designer Landing Page Generator ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Creating: Hero + Features + How It Works + Q&A ║
|
||||
║ Style: Modern, professional, conversion-optimized ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
### PHASE 2: Parallel Content Generation
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
|
||||
#### 2.1: Launch Content Generation Agents in Parallel
|
||||
|
||||
**CRITICAL: Launch ALL agents in a SINGLE message:**
|
||||
|
||||
```
|
||||
Launch these 4 Task agents IN PARALLEL:
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ AGENT 1: Branding Concept Generator │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Task tool with: │
|
||||
│ subagent_type: "frontend-architect" │
|
||||
│ run_in_background: true │
|
||||
│ prompt: | │
|
||||
│ # CONCEPT BRANDING GENERATION │
|
||||
│ │
|
||||
│ Read $DOCS_DIR/analysis.yml and create a branding concept. │
|
||||
│ │
|
||||
│ ## Output: branding.json │
|
||||
│ ```json │
|
||||
│ { │
|
||||
│ "brand": { │
|
||||
│ "name": "Project Name", │
|
||||
│ "tagline": "Compelling one-liner", │
|
||||
│ "value_proposition": "What makes it special" │
|
||||
│ }, │
|
||||
│ "colors": { │
|
||||
│ "primary": "#hex - main brand color", │
|
||||
│ "secondary": "#hex - accent color", │
|
||||
│ "accent": "#hex - CTA/highlight color", │
|
||||
│ "background": "#hex - light bg", │
|
||||
│ "background_dark": "#hex - dark mode bg", │
|
||||
│ "text": "#hex - primary text", │
|
||||
│ "text_muted": "#hex - secondary text" │
|
||||
│ }, │
|
||||
│ "typography": { │
|
||||
│ "heading_font": "Inter, system-ui, sans-serif", │
|
||||
│ "body_font": "Inter, system-ui, sans-serif", │
|
||||
│ "mono_font": "JetBrains Mono, monospace" │
|
||||
│ }, │
|
||||
│ "style": { │
|
||||
│ "border_radius": "12px", │
|
||||
│ "shadow": "0 4px 6px -1px rgba(0,0,0,0.1)", │
|
||||
│ "gradient": "linear-gradient(...)" │
|
||||
│ }, │
|
||||
│ "icons": { │
|
||||
│ "style": "lucide|heroicons|phosphor", │
|
||||
│ "feature_icons": ["icon1", "icon2", ...] │
|
||||
│ } │
|
||||
│ } │
|
||||
│ ``` │
|
||||
│ │
|
||||
│ Base colors on project type: │
|
||||
│ - Developer tools: Blues, purples │
|
||||
│ - Business apps: Blues, greens │
|
||||
│ - Creative tools: Vibrant, gradients │
|
||||
│ - Data/Analytics: Teals, blues │
|
||||
│ │
|
||||
│ Write to: $DOCS_DIR/branding.json │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ AGENT 2: Hero & Features Content │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Task tool with: │
|
||||
│ subagent_type: "technical-writer" │
|
||||
│ run_in_background: true │
|
||||
│ prompt: | │
|
||||
│ # HERO & FEATURES CONTENT │
|
||||
│ │
|
||||
│ Read $DOCS_DIR/analysis.yml and create marketing content. │
|
||||
│ │
|
||||
│ ## Output: content.json │
|
||||
│ ```json │
|
||||
│ { │
|
||||
│ "hero": { │
|
||||
│ "headline": "Powerful, benefit-focused headline", │
|
||||
│ "subheadline": "Explain the value in one sentence", │
|
||||
│ "cta_primary": "Get Started", │
|
||||
│ "cta_secondary": "Learn More", │
|
||||
│ "social_proof": "Used by X developers" │
|
||||
│ }, │
|
||||
│ "features": [ │
|
||||
│ { │
|
||||
│ "title": "Feature Name", │
|
||||
│ "description": "Benefit-focused description", │
|
||||
│ "icon": "suggested-icon-name" │
|
||||
│ } │
|
||||
│ ], │
|
||||
│ "how_it_works": [ │
|
||||
│ { │
|
||||
│ "step": 1, │
|
||||
│ "title": "Step Title", │
|
||||
│ "description": "What happens" │
|
||||
│ } │
|
||||
│ ], │
|
||||
│ "stats": [ │
|
||||
│ { "value": "10x", "label": "Faster" } │
|
||||
│ ] │
|
||||
│ } │
|
||||
│ ``` │
|
||||
│ │
|
||||
│ Writing Rules: │
|
||||
│ - Headlines: Benefit-focused, action-oriented │
|
||||
│ - Features: Max 6, each with clear benefit │
|
||||
│ - How It Works: 3-4 steps maximum │
|
||||
│ - Use numbers and specifics when possible │
|
||||
│ │
|
||||
│ Write to: $DOCS_DIR/content.json │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ AGENT 3: Q&A / FAQ Generator │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Task tool with: │
|
||||
│ subagent_type: "technical-writer" │
|
||||
│ run_in_background: true │
|
||||
│ prompt: | │
|
||||
│ # Q&A / FAQ GENERATION │
|
||||
│ │
|
||||
│ Read $DOCS_DIR/analysis.yml and PROJECT_DOCUMENTATION.md │
|
||||
│ to generate comprehensive Q&A. │
|
||||
│ │
|
||||
│ ## Output: faq.json │
|
||||
│ ```json │
|
||||
│ { │
|
||||
│ "categories": [ │
|
||||
│ { │
|
||||
│ "name": "Getting Started", │
|
||||
│ "questions": [ │
|
||||
│ { │
|
||||
│ "q": "How do I install [Project]?", │
|
||||
│ "a": "Clear, step-by-step answer" │
|
||||
│ } │
|
||||
│ ] │
|
||||
│ }, │
|
||||
│ { │
|
||||
│ "name": "Features", │
|
||||
│ "questions": [...] │
|
||||
│ }, │
|
||||
│ { │
|
||||
│ "name": "Technical", │
|
||||
│ "questions": [...] │
|
||||
│ }, │
|
||||
│ { │
|
||||
│ "name": "Pricing & Support", │
|
||||
│ "questions": [...] │
|
||||
│ } │
|
||||
│ ] │
|
||||
│ } │
|
||||
│ ``` │
|
||||
│ │
|
||||
│ Q&A Guidelines: │
|
||||
│ - 3-5 questions per category │
|
||||
│ - Anticipate real user questions │
|
||||
│ - Answers should be concise but complete │
|
||||
│ - Include code snippets where helpful │
|
||||
│ - Address common concerns and objections │
|
||||
│ │
|
||||
│ Write to: $DOCS_DIR/faq.json │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ AGENT 4: SEO & Meta Content │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Task tool with: │
|
||||
│ subagent_type: "technical-writer" │
|
||||
│ run_in_background: true │
|
||||
│ prompt: | │
|
||||
│ # SEO & META CONTENT │
|
||||
│ │
|
||||
│ Read $DOCS_DIR/analysis.yml and create SEO content. │
|
||||
│ │
|
||||
│ ## Output: seo.json │
|
||||
│ ```json │
|
||||
│ { │
|
||||
│ "title": "Project Name - Tagline | Category", │
|
||||
│ "description": "150-160 char meta description", │
|
||||
│ "keywords": ["keyword1", "keyword2"], │
|
||||
│ "og": { │
|
||||
│ "title": "Open Graph title", │
|
||||
│ "description": "OG description", │
|
||||
│ "type": "website" │
|
||||
│ }, │
|
||||
│ "twitter": { │
|
||||
│ "card": "summary_large_image", │
|
||||
│ "title": "Twitter title", │
|
||||
│ "description": "Twitter description" │
|
||||
│ }, │
|
||||
│ "structured_data": { │
|
||||
│ "@type": "SoftwareApplication", │
|
||||
│ "name": "...", │
|
||||
│ "description": "..." │
|
||||
│ } │
|
||||
│ } │
|
||||
│ ``` │
|
||||
│ │
|
||||
│ Write to: $DOCS_DIR/seo.json │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 2.2: Wait for All Content Agents
|
||||
|
||||
```
|
||||
Use TaskOutput tool to wait for each agent:
|
||||
- TaskOutput with task_id from Agent 1 (branding), block: true
|
||||
- TaskOutput with task_id from Agent 2 (content), block: true
|
||||
- TaskOutput with task_id from Agent 3 (faq), block: true
|
||||
- TaskOutput with task_id from Agent 4 (seo), block: true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
### PHASE 3: Generate Landing Page HTML
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
|
||||
#### 3.1: Generate Designer-Quality Landing Page
|
||||
|
||||
**Use Task tool with frontend-architect agent:**
|
||||
|
||||
```
|
||||
Task tool with:
|
||||
subagent_type: "frontend-architect"
|
||||
prompt: |
|
||||
# GENERATE LANDING PAGE HTML
|
||||
|
||||
Read these files and generate a stunning landing page:
|
||||
- $DOCS_DIR/branding.json (colors, typography, style)
|
||||
- $DOCS_DIR/content.json (hero, features, how-it-works)
|
||||
- $DOCS_DIR/faq.json (Q&A sections)
|
||||
- $DOCS_DIR/seo.json (meta tags)
|
||||
|
||||
## Output: $DOCS_DIR/landing.html
|
||||
|
||||
## Design Requirements
|
||||
|
||||
### Structure
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- SEO meta tags from seo.json -->
|
||||
<!-- Fonts: Google Fonts or system fonts -->
|
||||
<!-- Inline critical CSS -->
|
||||
</head>
|
||||
<body>
|
||||
<!-- Navigation (sticky) -->
|
||||
<nav>Logo | Links | CTA Button</nav>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="hero">
|
||||
<h1>Headline</h1>
|
||||
<p>Subheadline</p>
|
||||
<div class="cta-buttons">Primary | Secondary</div>
|
||||
<!-- Optional: Animated gradient background -->
|
||||
</section>
|
||||
|
||||
<!-- Social Proof / Stats -->
|
||||
<section class="stats">
|
||||
<div class="stat">Value + Label</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Grid -->
|
||||
<section class="features">
|
||||
<h2>Features</h2>
|
||||
<div class="feature-grid">
|
||||
<!-- 3-column grid of feature cards -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- How It Works -->
|
||||
<section class="how-it-works">
|
||||
<h2>How It Works</h2>
|
||||
<div class="steps">
|
||||
<!-- Numbered steps with visual flow -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Q&A / FAQ -->
|
||||
<section class="faq">
|
||||
<h2>Frequently Asked Questions</h2>
|
||||
<div class="faq-accordion">
|
||||
<!-- Expandable Q&A items -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section class="cta-final">
|
||||
<h2>Ready to Get Started?</h2>
|
||||
<button>Primary CTA</button>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
Links | Copyright | Social
|
||||
</footer>
|
||||
|
||||
<!-- Inline JavaScript for interactions -->
|
||||
<script>
|
||||
// FAQ accordion
|
||||
// Smooth scroll
|
||||
// Dark mode toggle
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### Visual Design Standards
|
||||
|
||||
**Hero Section**:
|
||||
- Full viewport height or 80vh minimum
|
||||
- Gradient or subtle pattern background
|
||||
- Large, bold headline (48-72px)
|
||||
- Clear visual hierarchy
|
||||
- Floating elements or subtle animation
|
||||
|
||||
**Feature Cards**:
|
||||
- Icon + Title + Description
|
||||
- Subtle hover effects (scale, shadow)
|
||||
- Consistent spacing (24-32px gaps)
|
||||
- 3-column on desktop, 1 on mobile
|
||||
|
||||
**How It Works**:
|
||||
- Visual step indicators (1, 2, 3)
|
||||
- Connecting lines or arrows
|
||||
- Icons or illustrations per step
|
||||
- Horizontal on desktop, vertical on mobile
|
||||
|
||||
**FAQ Accordion**:
|
||||
- Click to expand/collapse
|
||||
- Smooth animation (max-height transition)
|
||||
- Plus/minus or chevron indicator
|
||||
- Category grouping
|
||||
|
||||
**Micro-interactions**:
|
||||
- Button hover: scale(1.02) + shadow
|
||||
- Card hover: translateY(-4px) + shadow
|
||||
- Smooth scroll for anchor links
|
||||
- Fade-in on scroll (intersection observer)
|
||||
|
||||
### CSS Requirements
|
||||
|
||||
```css
|
||||
/* CSS Custom Properties from branding.json */
|
||||
:root {
|
||||
--color-primary: ...;
|
||||
--color-secondary: ...;
|
||||
--font-heading: ...;
|
||||
--radius: ...;
|
||||
}
|
||||
|
||||
/* Dark mode */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-bg: var(--color-bg-dark);
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive breakpoints */
|
||||
/* Mobile: < 640px */
|
||||
/* Tablet: 640-1024px */
|
||||
/* Desktop: > 1024px */
|
||||
```
|
||||
|
||||
### JavaScript Requirements
|
||||
- FAQ accordion functionality
|
||||
- Smooth scroll for navigation
|
||||
- Optional: Scroll-triggered animations
|
||||
- Dark mode toggle (optional)
|
||||
- Mobile menu toggle
|
||||
|
||||
### Performance
|
||||
- Single HTML file (no external dependencies)
|
||||
- Inline critical CSS
|
||||
- Minimal JavaScript
|
||||
- Optimized for Core Web Vitals
|
||||
- < 100KB total size
|
||||
|
||||
Write complete HTML to: $DOCS_DIR/landing.html
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
### PHASE 4: Finalization
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
|
||||
#### 4.1: Validate Generated Files
|
||||
|
||||
Verify all files exist:
|
||||
- `$DOCS_DIR/branding.json`
|
||||
- `$DOCS_DIR/content.json`
|
||||
- `$DOCS_DIR/faq.json`
|
||||
- `$DOCS_DIR/seo.json`
|
||||
- `$DOCS_DIR/landing.html`
|
||||
|
||||
#### 4.2: Display Completion Banner
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ ✅ LANDING PAGE GENERATED SUCCESSFULLY ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Output Directory: $DOCS_DIR ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Files Created: ║
|
||||
║ 🎨 branding.json (Colors, typography, style guide) ║
|
||||
║ 📝 content.json (Hero, features, how-it-works) ║
|
||||
║ ❓ faq.json (Q&A content by category) ║
|
||||
║ 🔍 seo.json (Meta tags, Open Graph, Schema) ║
|
||||
║ 🌐 landing.html (Designer-quality landing page) ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Landing Page Features: ║
|
||||
║ ✅ Hero with compelling headline + CTAs ║
|
||||
║ ✅ Feature grid with icons ║
|
||||
║ ✅ How It Works visual flow ║
|
||||
║ ✅ Interactive FAQ accordion ║
|
||||
║ ✅ Responsive (mobile-first) ║
|
||||
║ ✅ Dark mode support ║
|
||||
║ ✅ SEO optimized ║
|
||||
║ ✅ Single file, no dependencies ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Next Steps: ║
|
||||
║ → Open landing.html in browser to preview ║
|
||||
║ → Customize colors in branding.json ║
|
||||
║ → Add real screenshots/images ║
|
||||
║ → Deploy to your hosting ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ARGUMENTS
|
||||
|
||||
| Argument | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `[docs_dir]` | `docs` | Directory containing documentation from /eureka:index |
|
||||
| `--style` | `modern` | Design style: modern, minimal, bold, corporate |
|
||||
| `--theme` | `auto` | Color theme: auto, light, dark |
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
```bash
|
||||
# Generate landing page from default docs directory
|
||||
/eureka:landing
|
||||
|
||||
# Generate from custom documentation directory
|
||||
/eureka:landing my-docs
|
||||
|
||||
# Generate with specific style
|
||||
/eureka:landing --style minimal
|
||||
|
||||
# Generate dark-only theme
|
||||
/eureka:landing --theme dark
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DESIGN STYLE OPTIONS
|
||||
|
||||
### Modern (Default)
|
||||
- Gradient backgrounds
|
||||
- Rounded corners (12-16px)
|
||||
- Soft shadows
|
||||
- Vibrant accent colors
|
||||
- Floating elements
|
||||
|
||||
### Minimal
|
||||
- Clean white space
|
||||
- Thin borders
|
||||
- Muted colors
|
||||
- Typography-focused
|
||||
- Subtle interactions
|
||||
|
||||
### Bold
|
||||
- Strong colors
|
||||
- Large typography
|
||||
- High contrast
|
||||
- Geometric shapes
|
||||
- Impactful CTAs
|
||||
|
||||
### Corporate
|
||||
- Professional blues/grays
|
||||
- Structured layout
|
||||
- Trust indicators
|
||||
- Clean lines
|
||||
- Conservative animations
|
||||
|
||||
---
|
||||
|
||||
## OUTPUT STRUCTURE
|
||||
|
||||
```
|
||||
docs/
|
||||
├── analysis.yml (from /eureka:index)
|
||||
├── PROJECT_DOCUMENTATION.md
|
||||
├── API_REFERENCE.md
|
||||
├── COMPONENTS.md
|
||||
├── QUICK_REFERENCE.md
|
||||
├── index.html (documentation HTML)
|
||||
│
|
||||
├── branding.json (NEW - concept branding)
|
||||
├── content.json (NEW - marketing content)
|
||||
├── faq.json (NEW - Q&A content)
|
||||
├── seo.json (NEW - SEO metadata)
|
||||
└── landing.html (NEW - landing page)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## BRANDING SYSTEM
|
||||
|
||||
The generated branding.json provides a complete design system:
|
||||
|
||||
```json
|
||||
{
|
||||
"brand": {
|
||||
"name": "Project Name",
|
||||
"tagline": "Your compelling tagline",
|
||||
"value_proposition": "What makes it unique"
|
||||
},
|
||||
"colors": {
|
||||
"primary": "#6366f1",
|
||||
"secondary": "#8b5cf6",
|
||||
"accent": "#f59e0b",
|
||||
"background": "#ffffff",
|
||||
"background_dark": "#0f172a",
|
||||
"text": "#1e293b",
|
||||
"text_muted": "#64748b"
|
||||
},
|
||||
"typography": {
|
||||
"heading_font": "Inter, system-ui, sans-serif",
|
||||
"body_font": "Inter, system-ui, sans-serif",
|
||||
"mono_font": "JetBrains Mono, Consolas, monospace"
|
||||
},
|
||||
"style": {
|
||||
"border_radius": "12px",
|
||||
"shadow_sm": "0 1px 2px rgba(0,0,0,0.05)",
|
||||
"shadow_md": "0 4px 6px -1px rgba(0,0,0,0.1)",
|
||||
"shadow_lg": "0 10px 15px -3px rgba(0,0,0,0.1)",
|
||||
"gradient": "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This can be used to:
|
||||
- Maintain consistent branding across all pages
|
||||
- Generate CSS custom properties
|
||||
- Create Figma/design tool exports
|
||||
- Build component libraries
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
---
|
||||
description: Analyze codebase and generate project manifest from existing code
|
||||
allowed-tools: Read, Write, Bash, Glob, Grep, Task, AskUserQuestion
|
||||
---
|
||||
|
||||
# Analyze Codebase and Generate Manifest
|
||||
|
||||
Analyze the current codebase and generate all files needed for the guardrail workflow system.
|
||||
|
||||
## Arguments
|
||||
|
||||
- `$ARGUMENTS` - Optional flags:
|
||||
- `--force` - Overwrite existing manifest without asking
|
||||
- `--dry-run` - Preview manifest without writing
|
||||
- `--deep` - Use AI agent for deeper analysis
|
||||
- `<name>` - Custom project name
|
||||
|
||||
## Generated Files
|
||||
|
||||
This command creates:
|
||||
1. `project_manifest.json` - Entity definitions and dependencies
|
||||
2. `.workflow/index.yml` - Version tracking index
|
||||
3. `.workflow/versions/` - Directory for version snapshots
|
||||
|
||||
## Quick Execution (Default)
|
||||
|
||||
### Step 1: Run the Python analyzer script
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/analyze_codebase.py --path . $ARGUMENTS
|
||||
```
|
||||
|
||||
If the script succeeds, continue to Step 2.
|
||||
|
||||
### Step 2: Initialize Workflow Directory Structure [MANDATORY]
|
||||
```bash
|
||||
# Create workflow directory structure
|
||||
mkdir -p .workflow/versions
|
||||
|
||||
# Create index.yml if it doesn't exist
|
||||
if [ ! -f .workflow/index.yml ]; then
|
||||
cat > .workflow/index.yml << 'EOF'
|
||||
versions: []
|
||||
latest_version: null
|
||||
total_versions: 0
|
||||
EOF
|
||||
echo "Created .workflow/index.yml"
|
||||
fi
|
||||
```
|
||||
|
||||
### Step 3: Display Summary
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ ✅ GUARDRAIL INITIALIZED ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Files Created: ║
|
||||
║ ✓ project_manifest.json ║
|
||||
║ ✓ .workflow/index.yml ║
|
||||
║ ✓ .workflow/versions/ ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Ready to use: ║
|
||||
║ /workflow:spawn <feature> Start a new feature ║
|
||||
║ /guardrail:status Check project status ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
## Deep Analysis Mode (--deep flag)
|
||||
|
||||
**Use the Task tool to spawn an Explore agent for comprehensive codebase analysis**
|
||||
|
||||
Use Task tool with:
|
||||
subagent_type: "Explore"
|
||||
prompt: |
|
||||
Analyze this codebase thoroughly and return structured information about:
|
||||
|
||||
1. **Pages** (Next.js App Router):
|
||||
- Find all page.tsx files in app/ directory
|
||||
- Extract route paths from file locations
|
||||
- Identify components imported/used
|
||||
- Identify API dependencies (fetch calls)
|
||||
|
||||
2. **Components**:
|
||||
- Find all .tsx files in app/components/
|
||||
- Extract component names and exports
|
||||
- Extract prop interfaces/types
|
||||
- Identify child component dependencies
|
||||
|
||||
3. **API Routes**:
|
||||
- Find all route.ts files in app/api/
|
||||
- Extract HTTP methods (GET, POST, PUT, DELETE, PATCH)
|
||||
- Identify request/response types
|
||||
- Extract path parameters
|
||||
|
||||
4. **Database/Types**:
|
||||
- Find type definitions in app/lib/
|
||||
- Extract interfaces and type aliases
|
||||
- Identify database schemas/tables
|
||||
|
||||
5. **Dependencies**:
|
||||
- Which components are used by which pages
|
||||
- Which APIs are called by which components
|
||||
- Which database tables are used by which APIs
|
||||
|
||||
Return the analysis as structured JSON sections.
|
||||
|
||||
### Phase 2: Generate Manifest
|
||||
|
||||
Based on the analysis, create `project_manifest.json` with this structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"project": {
|
||||
"name": "<project-name>",
|
||||
"version": "1.0.0",
|
||||
"created_at": "<ISO timestamp>",
|
||||
"description": "<inferred from package.json or README>"
|
||||
},
|
||||
"state": {
|
||||
"current_phase": "IMPLEMENTATION_PHASE",
|
||||
"approval_status": {
|
||||
"manifest_approved": true,
|
||||
"approved_by": "analyzer",
|
||||
"approved_at": "<ISO timestamp>"
|
||||
},
|
||||
"revision_history": [
|
||||
{
|
||||
"action": "MANIFEST_GENERATED",
|
||||
"timestamp": "<ISO timestamp>",
|
||||
"details": "Generated from existing codebase analysis"
|
||||
}
|
||||
]
|
||||
},
|
||||
"entities": {
|
||||
"pages": [
|
||||
{
|
||||
"id": "page_<name>",
|
||||
"path": "/<route>",
|
||||
"file_path": "app/<path>/page.tsx",
|
||||
"status": "IMPLEMENTED",
|
||||
"description": "<inferred>",
|
||||
"components": ["comp_<name>", ...],
|
||||
"data_dependencies": ["api_<name>", ...]
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"id": "comp_<name>",
|
||||
"name": "<ComponentName>",
|
||||
"file_path": "app/components/<Name>.tsx",
|
||||
"status": "IMPLEMENTED",
|
||||
"description": "<inferred from component>",
|
||||
"props": {
|
||||
"<propName>": {
|
||||
"type": "<type>",
|
||||
"optional": true|false,
|
||||
"description": "<if available>"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"api_endpoints": [
|
||||
{
|
||||
"id": "api_<action>_<resource>",
|
||||
"path": "/api/<path>",
|
||||
"method": "GET|POST|PUT|DELETE|PATCH",
|
||||
"file_path": "app/api/<path>/route.ts",
|
||||
"status": "IMPLEMENTED",
|
||||
"description": "<inferred>",
|
||||
"request": {
|
||||
"params": {},
|
||||
"query": {},
|
||||
"body": {}
|
||||
},
|
||||
"response": {
|
||||
"type": "<type>",
|
||||
"description": "<description>"
|
||||
}
|
||||
}
|
||||
],
|
||||
"database_tables": [
|
||||
{
|
||||
"id": "table_<name>",
|
||||
"name": "<tableName>",
|
||||
"file_path": "app/lib/db.ts",
|
||||
"status": "IMPLEMENTED",
|
||||
"description": "<description>",
|
||||
"columns": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"component_to_page": {},
|
||||
"api_to_component": {},
|
||||
"table_to_api": {}
|
||||
},
|
||||
"types": {}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3: Entity Naming Conventions
|
||||
|
||||
Use these ID formats:
|
||||
- **Pages**: `page_<name>` (e.g., `page_home`, `page_tasks`, `page_task_detail`)
|
||||
- **Components**: `comp_<snake_case>` (e.g., `comp_task_list`, `comp_filter_bar`)
|
||||
- **APIs**: `api_<action>_<resource>` (e.g., `api_list_tasks`, `api_create_task`)
|
||||
- **Tables**: `table_<name>` (e.g., `table_tasks`, `table_users`)
|
||||
|
||||
### Phase 4: Write Manifest
|
||||
|
||||
1. Write the generated manifest to `project_manifest.json`
|
||||
2. Validate JSON syntax
|
||||
3. Display summary:
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ 📊 MANIFEST GENERATED ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Project: <name> ║
|
||||
║ Generated: <timestamp> ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ ENTITIES DISCOVERED ║
|
||||
║ 📄 Pages: X ║
|
||||
║ 🧩 Components: X ║
|
||||
║ 🔌 APIs: X ║
|
||||
║ 🗄️ Tables: X ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Status: All entities marked as IMPLEMENTED ║
|
||||
║ Phase: IMPLEMENTATION_PHASE ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
If manifest already exists, ask user:
|
||||
1. **Overwrite** - Replace existing manifest
|
||||
2. **Merge** - Add new entities, keep existing
|
||||
3. **Cancel** - Abort operation
|
||||
|
||||
## Notes
|
||||
|
||||
- All discovered entities are marked as `IMPLEMENTED` since they already exist
|
||||
- Project starts in `IMPLEMENTATION_PHASE` since code exists
|
||||
- Use this command to bring existing projects under guardrail management
|
||||
- After generation, use `/guardrail:validate` to verify manifest accuracy
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
description: Approve design and transition to IMPLEMENTATION_PHASE (Reviewer mode)
|
||||
allowed-tools: Read, Write, Bash
|
||||
---
|
||||
|
||||
# Approve Design (Reviewer Mode)
|
||||
|
||||
✅ **REVIEWER MODE ACTIVATED**
|
||||
|
||||
Approve the current design and enable implementation.
|
||||
|
||||
## CRITICAL RULES
|
||||
|
||||
You are acting as the **REVIEWER AGENT**.
|
||||
|
||||
✅ **ALLOWED**:
|
||||
- Read any file
|
||||
- Update approval status in manifest
|
||||
- Transition phases
|
||||
|
||||
❌ **BLOCKED**:
|
||||
- Write ANY code files
|
||||
- You cannot implement anything
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Verify Phase**: Must be in `DESIGN_REVIEW`
|
||||
|
||||
2. **Run Full Validation**:
|
||||
```bash
|
||||
python3 "$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_manifest.py" --strict
|
||||
```
|
||||
|
||||
3. **If Valid**, update manifest:
|
||||
- Set `state.approval_status.manifest_approved = true`
|
||||
- Set `state.approval_status.approved_by = "reviewer"`
|
||||
- Set `state.approval_status.approved_at = <current timestamp>`
|
||||
|
||||
4. **Transition to Implementation**:
|
||||
```bash
|
||||
python3 "$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/transition_phase.py" --to IMPLEMENTATION_PHASE
|
||||
```
|
||||
|
||||
5. **Show Results**:
|
||||
- List all entities now with status `APPROVED`
|
||||
- Explain that code can now be written for these entities
|
||||
- Suggest `/guardrail:implement` to start
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
description: Design a new feature by adding entities to manifest (Architect mode)
|
||||
allowed-tools: Read, Write, Bash
|
||||
---
|
||||
|
||||
# Design Feature (Architect Mode)
|
||||
|
||||
🏗️ **ARCHITECT MODE ACTIVATED**
|
||||
|
||||
Design the feature: "$ARGUMENTS"
|
||||
|
||||
## CRITICAL RULES
|
||||
|
||||
You are now acting as the **ARCHITECT AGENT**.
|
||||
|
||||
✅ **ALLOWED**:
|
||||
- Read any file
|
||||
- Write to `project_manifest.json` ONLY
|
||||
- Run validation scripts
|
||||
|
||||
❌ **BLOCKED**:
|
||||
- Write ANY code files (.ts, .tsx, .css, .sql, .js, .jsx)
|
||||
- You CANNOT write implementation code yet
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Verify Phase**: Check `project_manifest.json` - must be in `DESIGN_PHASE`
|
||||
|
||||
2. **Analyze Requirements**: Break down "$ARGUMENTS" into:
|
||||
- Pages needed (routes/screens)
|
||||
- Components needed (UI elements)
|
||||
- API endpoints needed (backend routes)
|
||||
- Database tables needed (data storage)
|
||||
|
||||
3. **Define Each Entity** in manifest with:
|
||||
- Unique ID following naming convention
|
||||
- Complete specification (props, schemas, columns)
|
||||
- `status: "DEFINED"`
|
||||
- File path where it will be implemented
|
||||
|
||||
4. **Update Manifest**: Add all entities to `project_manifest.json`
|
||||
|
||||
5. **Validate**: Run `python3 skills/guardrail-orchestrator/scripts/validate_manifest.py`
|
||||
|
||||
6. **Summarize**: List what was added and suggest `/guardrail:review`
|
||||
|
||||
## Entity Templates
|
||||
|
||||
### Page
|
||||
```json
|
||||
{
|
||||
"id": "page_[name]",
|
||||
"path": "/[route]",
|
||||
"file_path": "src/pages/[name]/index.tsx",
|
||||
"status": "DEFINED",
|
||||
"components": [],
|
||||
"data_dependencies": []
|
||||
}
|
||||
```
|
||||
|
||||
### Component
|
||||
```json
|
||||
{
|
||||
"id": "comp_[name]",
|
||||
"name": "[PascalCase]",
|
||||
"file_path": "src/components/[Name]/index.tsx",
|
||||
"status": "DEFINED",
|
||||
"props": {}
|
||||
}
|
||||
```
|
||||
|
||||
### API Endpoint
|
||||
```json
|
||||
{
|
||||
"id": "api_[action]_[resource]",
|
||||
"path": "/api/v1/[resource]",
|
||||
"method": "GET|POST|PUT|DELETE",
|
||||
"file_path": "src/api/[resource]/[action].ts",
|
||||
"status": "DEFINED"
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
---
|
||||
description: Implement an approved entity from the manifest
|
||||
allowed-tools: Read, Write, Bash
|
||||
---
|
||||
|
||||
# Implement Entity
|
||||
|
||||
Implement the entity: "$ARGUMENTS"
|
||||
|
||||
## CRITICAL RULES
|
||||
|
||||
⚠️ **GUARDRAIL ENFORCEMENT ACTIVE**
|
||||
|
||||
You can ONLY write to files that:
|
||||
1. Are defined in `project_manifest.json`
|
||||
2. Have status = `APPROVED`
|
||||
3. Match the `file_path` in the manifest EXACTLY
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Verify Phase**: Must be in `IMPLEMENTATION_PHASE`
|
||||
|
||||
2. **Find Entity** in manifest:
|
||||
- If "$ARGUMENTS" is `--all`: implement all APPROVED entities
|
||||
- Otherwise: find the specific entity by ID
|
||||
|
||||
3. **For Each Entity**:
|
||||
|
||||
a. **Load Definition** from manifest
|
||||
|
||||
b. **Verify Status** is `APPROVED`
|
||||
|
||||
c. **Generate Code** matching the specification:
|
||||
- Props must match manifest exactly
|
||||
- Types must match manifest exactly
|
||||
- File path must match `file_path` in manifest
|
||||
|
||||
d. **Write File** to the exact path in manifest
|
||||
|
||||
e. **Run Validations**:
|
||||
```bash
|
||||
npm run lint --if-present
|
||||
npm run type-check --if-present
|
||||
```
|
||||
|
||||
4. **Status Updates** (handled by post-hook):
|
||||
- Entity status changes to `IMPLEMENTED`
|
||||
- Timestamp recorded
|
||||
|
||||
## Code Templates
|
||||
|
||||
### Component (Frontend)
|
||||
```tsx
|
||||
import React from 'react';
|
||||
|
||||
interface [Name]Props {
|
||||
// From manifest.props
|
||||
}
|
||||
|
||||
export const [Name]: React.FC<[Name]Props> = (props) => {
|
||||
return (
|
||||
// Implementation
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### API Endpoint (Backend)
|
||||
```typescript
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
export async function handler(req: Request, res: Response) {
|
||||
// From manifest.request/response schemas
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
---
|
||||
description: Initialize a new guardrailed project with manifest
|
||||
allowed-tools: Bash, Write, Read
|
||||
---
|
||||
|
||||
# Initialize Guardrailed Project
|
||||
|
||||
Initialize a new project called "$ARGUMENTS" with guardrail enforcement and workflow system.
|
||||
|
||||
## Generated Files
|
||||
|
||||
This command creates:
|
||||
1. `project_manifest.json` - Entity definitions and dependencies
|
||||
2. `.workflow/index.yml` - Version tracking index
|
||||
3. `.workflow/versions/` - Directory for version snapshots
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Run the initialization script
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/init_project.py --name "$ARGUMENTS" --path .
|
||||
```
|
||||
|
||||
### Step 2: Initialize Workflow Directory Structure [MANDATORY]
|
||||
```bash
|
||||
# Create workflow directory structure
|
||||
mkdir -p .workflow/versions
|
||||
|
||||
# Create index.yml if it doesn't exist
|
||||
if [ ! -f .workflow/index.yml ]; then
|
||||
cat > .workflow/index.yml << 'EOF'
|
||||
versions: []
|
||||
latest_version: null
|
||||
total_versions: 0
|
||||
EOF
|
||||
fi
|
||||
```
|
||||
|
||||
### Step 3: Verify and Display Summary
|
||||
```bash
|
||||
# Verify files exist
|
||||
ls project_manifest.json .workflow/index.yml
|
||||
```
|
||||
|
||||
Display:
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ ✅ PROJECT INITIALIZED ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Project: $ARGUMENTS ║
|
||||
║ Phase: DESIGN_PHASE ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Files Created: ║
|
||||
║ ✓ project_manifest.json ║
|
||||
║ ✓ .workflow/index.yml ║
|
||||
║ ✓ .workflow/versions/ ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Next Steps: ║
|
||||
║ /guardrail:design Design features in manifest ║
|
||||
║ /workflow:spawn <feat> Start automated workflow ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Project starts in **DESIGN_PHASE** (manifest edits only)
|
||||
- Use `/guardrail:design` for manual design workflow
|
||||
- Use `/workflow:spawn` for automated design + implementation
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
description: Request design review and transition to DESIGN_REVIEW phase
|
||||
allowed-tools: Read, Write, Bash
|
||||
---
|
||||
|
||||
# Request Design Review
|
||||
|
||||
Transition the project from DESIGN_PHASE to DESIGN_REVIEW.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Validate Manifest**:
|
||||
```bash
|
||||
python3 "$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_manifest.py" --strict
|
||||
```
|
||||
|
||||
2. **If Valid**, transition phase:
|
||||
```bash
|
||||
python3 "$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/transition_phase.py" --to DESIGN_REVIEW
|
||||
```
|
||||
|
||||
3. **Show Review Checklist**:
|
||||
- [ ] All pages have at least one component
|
||||
- [ ] All components have defined props with types
|
||||
- [ ] All APIs have request/response schemas
|
||||
- [ ] All database tables have primary keys
|
||||
- [ ] No orphan components
|
||||
- [ ] No circular dependencies
|
||||
|
||||
4. **Explain Next Steps**:
|
||||
- Use `/guardrail:approve` to approve and move to implementation
|
||||
- Use `/guardrail:reject <feedback>` to send back for fixes
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
description: Show current project phase, entity counts, and pending work
|
||||
allowed-tools: Read, Bash
|
||||
---
|
||||
|
||||
# Guardrail Status
|
||||
|
||||
Display the current guardrail project status.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Read `project_manifest.json`
|
||||
|
||||
2. Display:
|
||||
- **Current Phase**: `state.current_phase`
|
||||
- **Approval Status**: `state.approval_status.manifest_approved`
|
||||
- **Entity Counts**:
|
||||
- Pages: count by status (DEFINED/APPROVED/IMPLEMENTED)
|
||||
- Components: count by status
|
||||
- API Endpoints: count by status
|
||||
- Database Tables: count by status
|
||||
- **Recent History**: last 5 items from `state.revision_history`
|
||||
|
||||
3. Show available actions for current phase:
|
||||
- DESIGN_PHASE: Can use `/guardrail:design`, then `/guardrail:review`
|
||||
- DESIGN_REVIEW: Can use `/guardrail:approve` or `/guardrail:reject`
|
||||
- IMPLEMENTATION_PHASE: Can use `/guardrail:implement`
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
description: Validate manifest integrity and completeness
|
||||
allowed-tools: Bash, Read
|
||||
---
|
||||
|
||||
# Validate Manifest
|
||||
|
||||
Run validation checks on `project_manifest.json`.
|
||||
|
||||
## Command
|
||||
|
||||
```bash
|
||||
python3 "$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_manifest.py" $ARGUMENTS
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
- No arguments: Basic validation
|
||||
- `--strict`: Treat warnings as errors
|
||||
|
||||
## What It Checks
|
||||
|
||||
1. **Structure**: Required top-level keys exist
|
||||
2. **Pages**: Have paths, components, file_paths
|
||||
3. **Components**: Have props with types, valid dependencies
|
||||
4. **APIs**: Have methods, paths, request/response schemas
|
||||
5. **Database**: Tables have primary keys, valid foreign keys
|
||||
6. **Dependencies**: No orphans, no circular references
|
||||
7. **Naming**: Follows conventions
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
description: Verify implementation matches manifest specifications
|
||||
allowed-tools: Bash, Read
|
||||
---
|
||||
|
||||
# Verify Implementation
|
||||
|
||||
Run verification to ensure all implemented code matches the manifest specifications.
|
||||
|
||||
## Command
|
||||
|
||||
```bash
|
||||
python3 "$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/verify_implementation.py" --project-root "$CLAUDE_PROJECT_DIR" $ARGUMENTS
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
- No arguments: Basic verification
|
||||
- `--verbose` or `-v`: Include warnings
|
||||
- `--json`: Output as JSON for programmatic use
|
||||
|
||||
## What It Checks
|
||||
|
||||
For each entity in the manifest:
|
||||
|
||||
### Components
|
||||
- File exists at specified `file_path`
|
||||
- Component name is exported
|
||||
- Props interface matches manifest definition
|
||||
|
||||
### Pages
|
||||
- File exists at specified `file_path`
|
||||
- Has `export default` (Next.js requirement)
|
||||
- Uses specified component dependencies
|
||||
|
||||
### API Endpoints
|
||||
- File exists at specified `file_path`
|
||||
- HTTP method handler exists (GET, POST, PUT, DELETE)
|
||||
- Request parameters are handled
|
||||
|
||||
### Database Tables
|
||||
- File exists at specified `file_path`
|
||||
- Column definitions present
|
||||
- CRUD operations implemented
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
✅ [component] comp_button
|
||||
File: app/components/Button.tsx
|
||||
|
||||
❌ [component] comp_missing
|
||||
File: app/components/Missing.tsx
|
||||
❌ ERROR: File not found
|
||||
|
||||
SUMMARY: 17/18 passed, 1 failed, 3 warnings
|
||||
```
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
---
|
||||
description: Approve a workflow gate (design or implementation)
|
||||
allowed-tools: Read, Write, Bash
|
||||
---
|
||||
|
||||
# Approve Workflow Gate
|
||||
|
||||
Approve gate: "$ARGUMENTS"
|
||||
|
||||
## Valid Gates
|
||||
- `design` - Approve the design phase (entities + tasks)
|
||||
- `implementation` - Approve the implementation phase (all code)
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Validate Gate
|
||||
- If "$ARGUMENTS" is not `design` or `implementation`: STOP and show usage
|
||||
|
||||
### 2. Check Workflow State
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py exists
|
||||
```
|
||||
|
||||
If no active workflow:
|
||||
```
|
||||
❌ No active workflow found.
|
||||
Start a new workflow with: /workflow:spawn "feature name"
|
||||
```
|
||||
|
||||
### 3. Verify Current Phase
|
||||
|
||||
**For design approval**:
|
||||
- Current phase must be `AWAITING_DESIGN_APPROVAL`
|
||||
- If not: Report error with current phase
|
||||
|
||||
**For implementation approval**:
|
||||
- Current phase must be `AWAITING_IMPL_APPROVAL`
|
||||
- If not: Report error with current phase
|
||||
|
||||
### 4. Execute Approval
|
||||
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py approve $ARGUMENTS
|
||||
```
|
||||
|
||||
### 5. Transition to Next Phase
|
||||
|
||||
**If design approved**:
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py transition DESIGN_APPROVED
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py transition IMPLEMENTING
|
||||
```
|
||||
|
||||
**If implementation approved**:
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py transition IMPL_APPROVED
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py transition COMPLETING
|
||||
```
|
||||
|
||||
### 6. Report
|
||||
|
||||
**Design Approved**:
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ ✅ DESIGN APPROVED ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ The design has been approved. Implementation will begin. ║
|
||||
║ ║
|
||||
║ Next steps: ║
|
||||
║ /workflow:frontend --next Start frontend tasks ║
|
||||
║ /workflow:backend --next Start backend tasks ║
|
||||
║ /workflow:resume Auto-continue workflow ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
**Implementation Approved**:
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ ✅ IMPLEMENTATION APPROVED ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ All implementations have been approved. ║
|
||||
║ ║
|
||||
║ Next steps: ║
|
||||
║ /workflow:complete --all Mark all tasks as done ║
|
||||
║ /workflow:resume Auto-complete workflow ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
description: Implement backend tasks (Backend agent)
|
||||
allowed-tools: Read, Write, Edit, Bash
|
||||
---
|
||||
|
||||
# Backend Agent - Implementation Mode
|
||||
|
||||
⚙️ **BACKEND AGENT ACTIVATED**
|
||||
|
||||
Implement task: "$ARGUMENTS"
|
||||
|
||||
## CRITICAL RULES
|
||||
|
||||
You are now the **BACKEND AGENT**.
|
||||
|
||||
✅ **ALLOWED**:
|
||||
- Read any file
|
||||
- Write new files (API routes, DB)
|
||||
- Edit existing backend files
|
||||
- Run Bash (build, lint, type-check, tests)
|
||||
|
||||
✅ **ALLOWED FILES**:
|
||||
- `app/api/**/*`
|
||||
- `app/lib/**/*`
|
||||
- `prisma/**/*`
|
||||
- `db/**/*`
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Load Task
|
||||
First, get the version-specific tasks directory:
|
||||
```bash
|
||||
TASKS_DIR=$(python3 skills/guardrail-orchestrator/scripts/version_manager.py tasks-dir)
|
||||
```
|
||||
|
||||
Read the task file: `$TASKS_DIR/$ARGUMENTS.yml`
|
||||
- If "$ARGUMENTS" is `--next`: find first task with `agent: backend` and `status: pending`
|
||||
|
||||
### Step 2: Update Workflow State
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py task <task_id> in_progress
|
||||
```
|
||||
|
||||
### Step 3: Verify Prerequisites
|
||||
- Check entity is `APPROVED` in `project_manifest.json`
|
||||
- Check all `dependencies` tasks are `completed`
|
||||
- If blocked:
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py task <task_id> blocked
|
||||
```
|
||||
Stop and report blocker.
|
||||
|
||||
### Step 4: Implement
|
||||
For each file in `file_paths`:
|
||||
1. Read manifest entity specification
|
||||
2. Generate code matching spec exactly:
|
||||
- HTTP methods must match manifest
|
||||
- Request params must match manifest
|
||||
- Response types must match manifest
|
||||
3. Follow existing project patterns
|
||||
|
||||
### Step 5: Validate
|
||||
Run validations:
|
||||
```bash
|
||||
npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Step 6: Update Task Status
|
||||
Update the task file:
|
||||
```yaml
|
||||
status: review
|
||||
completed_at: <current timestamp>
|
||||
```
|
||||
|
||||
Update workflow state:
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py task <task_id> review
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py progress --tasks-impl <count>
|
||||
```
|
||||
|
||||
### Step 7: Report
|
||||
- List implemented files
|
||||
- Show validation results
|
||||
- Suggest: `/workflow:review $ARGUMENTS`
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
---
|
||||
description: Mark approved task as completed
|
||||
allowed-tools: Read, Write, Bash
|
||||
---
|
||||
|
||||
# Complete Task
|
||||
|
||||
Mark task "$ARGUMENTS" as completed.
|
||||
|
||||
## Prerequisites
|
||||
- Task must have `status: approved`
|
||||
- All acceptance criteria verified by reviewer
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Read Task
|
||||
First, get the version-specific tasks directory:
|
||||
```bash
|
||||
TASKS_DIR=$(python3 skills/guardrail-orchestrator/scripts/version_manager.py tasks-dir)
|
||||
```
|
||||
|
||||
Read `$TASKS_DIR/$ARGUMENTS.yml`
|
||||
|
||||
### 2. Verify Status
|
||||
- If `status` is NOT `approved`: STOP and report error
|
||||
- If `status` is `approved`: proceed
|
||||
|
||||
### 3. Update Task
|
||||
Update the task file with:
|
||||
```yaml
|
||||
status: completed
|
||||
completed_at: <current timestamp>
|
||||
```
|
||||
|
||||
### 4. Update Workflow State
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py task <task_id> completed
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py progress --tasks-completed <count>
|
||||
```
|
||||
|
||||
### 5. Update Manifest (if applicable)
|
||||
For each entity in `entity_ids`:
|
||||
- Update entity status to `IMPLEMENTED` in `project_manifest.json`
|
||||
|
||||
### 6. Check Workflow Completion
|
||||
Check if all tasks are now completed:
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py status
|
||||
```
|
||||
|
||||
If all tasks completed, transition to implementation approval:
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py transition AWAITING_IMPL_APPROVAL
|
||||
```
|
||||
|
||||
### 7. Report
|
||||
```
|
||||
✅ Task completed: $ARGUMENTS
|
||||
|
||||
Entities implemented:
|
||||
- <entity_id_1>
|
||||
- <entity_id_2>
|
||||
|
||||
Next: /workflow:status to see remaining tasks
|
||||
/workflow:complete --all to complete all approved tasks
|
||||
```
|
||||
|
|
@ -0,0 +1,476 @@
|
|||
---
|
||||
description: Design system architecture with ER diagram, API contracts, and UI structure
|
||||
allowed-tools: Read, Write, Edit, Bash, Task, TodoWrite
|
||||
---
|
||||
|
||||
# Workflow Design - System Architecture Phase
|
||||
|
||||
**Input**: "$ARGUMENTS"
|
||||
|
||||
---
|
||||
|
||||
## PURPOSE
|
||||
|
||||
This command creates a comprehensive **design document** that serves as the source of truth for implementation. It defines:
|
||||
|
||||
1. **Data Layer** - ER diagram with models, fields, relations
|
||||
2. **API Layer** - REST endpoints with request/response contracts
|
||||
3. **UI Layer** - Pages and components with data requirements
|
||||
4. **Dependency Graph** - Layered execution order for parallel tasks
|
||||
|
||||
---
|
||||
|
||||
## ⛔ CRITICAL RULES
|
||||
|
||||
### MUST DO
|
||||
1. **MUST** create `design_document.yml` with ALL layers defined
|
||||
2. **MUST** run `validate_design.py` to generate dependency graph
|
||||
3. **MUST** verify no circular dependencies before proceeding
|
||||
4. **MUST** show layered execution plan to user
|
||||
|
||||
### CANNOT DO
|
||||
1. **CANNOT** create tasks without design document
|
||||
2. **CANNOT** skip validation step
|
||||
3. **CANNOT** proceed if validation fails
|
||||
|
||||
---
|
||||
|
||||
## EXECUTION FLOW
|
||||
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
### STEP 1: Initialize Design Session
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
|
||||
#### 1.1: Get Current Version
|
||||
```bash
|
||||
# Get active version from workflow
|
||||
VERSION_ID=$(cat .workflow/current.yml 2>/dev/null | grep "active_version:" | awk '{print $2}')
|
||||
if [ -z "$VERSION_ID" ]; then
|
||||
echo "ERROR: No active workflow. Run /workflow:spawn first"
|
||||
exit 1
|
||||
fi
|
||||
echo "VERSION_ID=$VERSION_ID"
|
||||
```
|
||||
|
||||
#### 1.2: Create Design Directory
|
||||
```bash
|
||||
mkdir -p .workflow/versions/$VERSION_ID/design
|
||||
mkdir -p .workflow/versions/$VERSION_ID/contexts
|
||||
mkdir -p .workflow/versions/$VERSION_ID/tasks
|
||||
```
|
||||
|
||||
#### 1.3: Display Design Start Banner
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ 📐 SYSTEM DESIGN PHASE ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Feature: $ARGUMENTS ║
|
||||
║ Version: $VERSION_ID ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ You will define: ║
|
||||
║ Layer 1: Data Models (ER Diagram) ║
|
||||
║ Layer 2: API Endpoints (REST Contracts) ║
|
||||
║ Layer 3: Pages & Components (UI Structure) ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
### STEP 2: Analyze Requirements & Design System
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
|
||||
**Use Task tool with system-architect agent:**
|
||||
|
||||
```
|
||||
Use Task tool with:
|
||||
subagent_type: "system-architect"
|
||||
prompt: |
|
||||
# SYSTEM ARCHITECT - Design Document Creation
|
||||
|
||||
## INPUT
|
||||
Feature: "$ARGUMENTS"
|
||||
Version: $VERSION_ID
|
||||
Output: .workflow/versions/$VERSION_ID/design/design_document.yml
|
||||
|
||||
## YOUR MISSION
|
||||
Create a comprehensive design document following the schema in:
|
||||
skills/guardrail-orchestrator/schemas/design_document.yml
|
||||
|
||||
## ANALYSIS PROCESS
|
||||
|
||||
### Phase A: Understand Requirements
|
||||
1. Read the feature description carefully
|
||||
2. Identify the core user flows
|
||||
3. Determine what data needs to be stored
|
||||
4. Identify what APIs are needed
|
||||
5. Plan the UI structure
|
||||
|
||||
### Phase B: Design Data Layer (ER Diagram)
|
||||
For each entity needed:
|
||||
- Define fields with types and constraints
|
||||
- Define relations to other entities
|
||||
- Define validations
|
||||
- Consider indexes for performance
|
||||
|
||||
### Phase C: Design API Layer
|
||||
For each endpoint needed:
|
||||
- Define HTTP method and path
|
||||
- Define request body schema (for POST/PUT/PATCH)
|
||||
- Define response schemas for all status codes
|
||||
- Define authentication requirements
|
||||
- Link to data models used
|
||||
|
||||
### Phase D: Design UI Layer
|
||||
For each page needed:
|
||||
- Define route path
|
||||
- List data requirements (which APIs)
|
||||
- List components used
|
||||
- Define auth requirements
|
||||
|
||||
For each component needed:
|
||||
- Define props interface
|
||||
- Define events emitted
|
||||
- List child components
|
||||
- List APIs called directly (if any)
|
||||
|
||||
## OUTPUT FORMAT
|
||||
|
||||
Create `.workflow/versions/$VERSION_ID/design/design_document.yml`:
|
||||
|
||||
```yaml
|
||||
# Design Document
|
||||
workflow_version: "$VERSION_ID"
|
||||
feature: "$ARGUMENTS"
|
||||
created_at: <timestamp>
|
||||
status: draft
|
||||
revision: 1
|
||||
|
||||
# LAYER 1: DATA MODELS
|
||||
data_models:
|
||||
- id: model_<name>
|
||||
name: <PascalCase>
|
||||
description: "<what this model represents>"
|
||||
table_name: <snake_case>
|
||||
fields:
|
||||
- name: id
|
||||
type: uuid
|
||||
constraints: [primary_key]
|
||||
- name: <field_name>
|
||||
type: <string|integer|boolean|datetime|uuid|json|text|float|enum>
|
||||
constraints: [<unique|not_null|indexed|default>]
|
||||
# If enum:
|
||||
enum_values: [<value1>, <value2>]
|
||||
relations:
|
||||
- type: <has_one|has_many|belongs_to|many_to_many>
|
||||
target: model_<other>
|
||||
foreign_key: <fk_field>
|
||||
on_delete: <cascade|set_null|restrict>
|
||||
timestamps: true
|
||||
validations:
|
||||
- field: <field_name>
|
||||
rule: "<validation_rule>"
|
||||
message: "<error message>"
|
||||
|
||||
# LAYER 2: API ENDPOINTS
|
||||
api_endpoints:
|
||||
- id: api_<verb>_<resource>
|
||||
method: <GET|POST|PUT|PATCH|DELETE>
|
||||
path: /api/<path>
|
||||
summary: "<short description>"
|
||||
description: "<detailed description>"
|
||||
# For POST/PUT/PATCH:
|
||||
request_body:
|
||||
content_type: application/json
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
- name: <field>
|
||||
type: <type>
|
||||
required: <true|false>
|
||||
validations: [<rules>]
|
||||
responses:
|
||||
- status: 200
|
||||
description: "Success"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
- name: <field>
|
||||
type: <type>
|
||||
- status: 400
|
||||
description: "Validation error"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
- name: error
|
||||
type: string
|
||||
depends_on_models: [model_<name>]
|
||||
auth:
|
||||
required: <true|false>
|
||||
roles: [<role1>, <role2>]
|
||||
|
||||
# LAYER 3: PAGES
|
||||
pages:
|
||||
- id: page_<name>
|
||||
name: "<Human Name>"
|
||||
path: /<route>
|
||||
layout: <layout_component>
|
||||
data_needs:
|
||||
- api_id: api_<name>
|
||||
purpose: "<why needed>"
|
||||
on_load: <true|false>
|
||||
components: [component_<name1>, component_<name2>]
|
||||
auth:
|
||||
required: <true|false>
|
||||
roles: []
|
||||
redirect: /login
|
||||
|
||||
# LAYER 3: COMPONENTS
|
||||
components:
|
||||
- id: component_<name>
|
||||
name: <PascalCaseName>
|
||||
props:
|
||||
- name: <propName>
|
||||
type: <TypeScript type>
|
||||
required: <true|false>
|
||||
description: "<what this prop does>"
|
||||
events:
|
||||
- name: <onEventName>
|
||||
payload: "<payload type>"
|
||||
description: "<when this fires>"
|
||||
uses_apis: []
|
||||
uses_components: [component_<child>]
|
||||
variants: [<variant1>, <variant2>]
|
||||
```
|
||||
|
||||
## DESIGN PRINCIPLES
|
||||
|
||||
1. **Start with Data**: What data is needed? Design models first.
|
||||
2. **APIs Serve UI**: What operations does UI need? Design APIs next.
|
||||
3. **UI Consumes APIs**: Pages/Components use APIs. Design UI last.
|
||||
4. **Explicit Dependencies**: Every relation must be clearly defined.
|
||||
5. **Contracts First**: API request/response schemas are contracts.
|
||||
|
||||
## VERIFICATION
|
||||
|
||||
After creating the design document, verify:
|
||||
1. Every API references existing models
|
||||
2. Every page references existing APIs and components
|
||||
3. Every component references existing child components
|
||||
4. No circular dependencies
|
||||
|
||||
## OUTPUT
|
||||
|
||||
After creating the file, output:
|
||||
```
|
||||
=== DESIGN DOCUMENT CREATED ===
|
||||
|
||||
Data Models: X
|
||||
API Endpoints: X
|
||||
Pages: X
|
||||
Components: X
|
||||
|
||||
File: .workflow/versions/$VERSION_ID/design/design_document.yml
|
||||
```
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
### STEP 3: Validate Design & Generate Dependency Graph
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
|
||||
#### 3.1: Run Design Validation [MANDATORY]
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/validate_design.py \
|
||||
.workflow/versions/$VERSION_ID/design/design_document.yml \
|
||||
--output-dir .workflow/versions/$VERSION_ID
|
||||
```
|
||||
|
||||
**This generates:**
|
||||
- `.workflow/versions/$VERSION_ID/dependency_graph.yml` - Layered execution order
|
||||
- `.workflow/versions/$VERSION_ID/contexts/*.yml` - Per-entity context snapshots
|
||||
- `.workflow/versions/$VERSION_ID/tasks/*.yml` - Tasks with full context
|
||||
|
||||
#### 3.2: Check Validation Result
|
||||
```bash
|
||||
VALIDATION_EXIT=$?
|
||||
if [ $VALIDATION_EXIT -ne 0 ]; then
|
||||
echo "❌ Design validation failed. Fix errors and re-run."
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
**BLOCK IF**: Validation fails → Display errors, do not proceed
|
||||
|
||||
---
|
||||
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
### STEP 4: Display Layered Execution Plan
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
|
||||
Read the generated dependency graph and display:
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ 📊 DEPENDENCY GRAPH - EXECUTION LAYERS ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
║ Layer 1: DATA MODELS (Parallel) ║
|
||||
║ ───────────────────────────────────────────── ║
|
||||
║ 📦 model_user → backend ║
|
||||
║ 📦 model_post → backend ║
|
||||
║ ║
|
||||
║ Layer 2: API ENDPOINTS (Parallel, after Layer 1) ║
|
||||
║ ───────────────────────────────────────────── ║
|
||||
║ 🔌 api_create_user → backend (needs: model_user) ║
|
||||
║ 🔌 api_list_users → backend (needs: model_user) ║
|
||||
║ 🔌 api_create_post → backend (needs: model_user, model_post)║
|
||||
║ ║
|
||||
║ Layer 3: UI (Parallel, after Layer 2) ║
|
||||
║ ───────────────────────────────────────────── ║
|
||||
║ 🧩 component_user_card → frontend ║
|
||||
║ 📄 page_users → frontend (needs: api_list_users) ║
|
||||
║ ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ EXECUTION SUMMARY ║
|
||||
║ Total entities: X ║
|
||||
║ Total layers: X ║
|
||||
║ Max parallelism: X (tasks can run simultaneously) ║
|
||||
║ Critical path: X layers deep ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
### STEP 5: Display Design Summary for Approval
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ 🛑 DESIGN APPROVAL REQUIRED ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Feature: $ARGUMENTS ║
|
||||
║ Version: $VERSION_ID ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ DESIGN DOCUMENT ║
|
||||
║ 📦 Data Models: X ║
|
||||
║ 🔌 API Endpoints: X ║
|
||||
║ 📄 Pages: X ║
|
||||
║ 🧩 Components: X ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ GENERATED ARTIFACTS ║
|
||||
║ ✅ Dependency graph calculated ║
|
||||
║ ✅ Context snapshots created (X files) ║
|
||||
║ ✅ Implementation tasks created (X tasks) ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ EXECUTION PLAN ║
|
||||
║ Layer 1: X tasks (parallel) → backend ║
|
||||
║ Layer 2: X tasks (parallel) → backend ║
|
||||
║ Layer 3: X tasks (parallel) → frontend ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ FILES CREATED ║
|
||||
║ .workflow/versions/$VERSION_ID/design/design_document.yml ║
|
||||
║ .workflow/versions/$VERSION_ID/dependency_graph.yml ║
|
||||
║ .workflow/versions/$VERSION_ID/contexts/*.yml ║
|
||||
║ .workflow/versions/$VERSION_ID/tasks/*.yml ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ NEXT STEPS ║
|
||||
║ Review the design above, then: ║
|
||||
║ /workflow:approve - Proceed to implementation ║
|
||||
║ /workflow:reject - Request design changes ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
### STEP 6: Transition Workflow State
|
||||
### ═══════════════════════════════════════════════════════════════
|
||||
|
||||
```bash
|
||||
# Update progress
|
||||
TASK_COUNT=$(ls .workflow/versions/$VERSION_ID/tasks/*.yml 2>/dev/null | wc -l)
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py progress \
|
||||
--tasks-created $TASK_COUNT
|
||||
|
||||
# Transition to awaiting approval
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py transition AWAITING_DESIGN_APPROVAL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CONTEXT SNAPSHOT EXAMPLE
|
||||
|
||||
Each task gets a context file like `.workflow/versions/$VERSION_ID/contexts/api_create_user.yml`:
|
||||
|
||||
```yaml
|
||||
task_id: task_create_api_create_user
|
||||
entity_id: api_create_user
|
||||
workflow_version: v001
|
||||
|
||||
target:
|
||||
type: api
|
||||
definition:
|
||||
method: POST
|
||||
path: /api/users
|
||||
request_body:
|
||||
properties:
|
||||
- name: email
|
||||
type: string
|
||||
required: true
|
||||
validations: [email]
|
||||
- name: password
|
||||
type: string
|
||||
required: true
|
||||
validations: [min:8]
|
||||
responses:
|
||||
- status: 201
|
||||
schema: { id, email, name, created_at }
|
||||
- status: 400
|
||||
schema: { error, details }
|
||||
- status: 409
|
||||
schema: { error }
|
||||
|
||||
related:
|
||||
models:
|
||||
- id: model_user
|
||||
definition:
|
||||
name: User
|
||||
fields:
|
||||
- { name: id, type: uuid }
|
||||
- { name: email, type: string }
|
||||
- { name: password_hash, type: string }
|
||||
|
||||
dependencies:
|
||||
entity_ids: [model_user]
|
||||
|
||||
files:
|
||||
to_create:
|
||||
- app/api/users/route.ts
|
||||
reference:
|
||||
- path: app/api/health/route.ts
|
||||
purpose: "API route pattern"
|
||||
|
||||
acceptance:
|
||||
- criterion: "POST /api/users returns 201 on success"
|
||||
verification: "curl -X POST /api/users with valid data"
|
||||
- criterion: "Returns 409 if email exists"
|
||||
verification: "Test with duplicate email"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## USAGE
|
||||
|
||||
```bash
|
||||
# After /workflow:spawn, run design:
|
||||
/workflow:design
|
||||
|
||||
# This will:
|
||||
# 1. Create comprehensive design document
|
||||
# 2. Validate and generate dependency graph
|
||||
# 3. Create tasks with full context
|
||||
# 4. Wait for approval before implementation
|
||||
```
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
---
|
||||
description: Compare workflow versions and show manifest changes
|
||||
allowed-tools: Read, Bash
|
||||
---
|
||||
|
||||
# Workflow Version Diff
|
||||
|
||||
Compare workflow versions to see what changed in the project manifest.
|
||||
|
||||
## EXECUTION PROTOCOL
|
||||
|
||||
### Step 1: Parse Arguments
|
||||
|
||||
```
|
||||
IF "$ARGUMENTS" = "":
|
||||
MODE = "current" (diff latest version with current)
|
||||
ELSE IF "$ARGUMENTS" matches "v\d+ v\d+":
|
||||
MODE = "versions" (diff two specific versions)
|
||||
ELSE IF "$ARGUMENTS" matches "v\d+":
|
||||
MODE = "single" (diff specific version with current)
|
||||
ELSE IF "$ARGUMENTS" = "--changelog" or "--log":
|
||||
MODE = "changelog" (show all version changelogs)
|
||||
ELSE IF "$ARGUMENTS" contains "--json":
|
||||
OUTPUT = "json"
|
||||
```
|
||||
|
||||
### Step 2: Get Available Versions
|
||||
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/manifest_diff.py versions
|
||||
```
|
||||
|
||||
### Step 3: Execute Diff Based on Mode
|
||||
|
||||
**MODE: current (default)**
|
||||
```bash
|
||||
# Get latest version
|
||||
LATEST=$(ls -1 .workflow/versions/ 2>/dev/null | tail -1)
|
||||
|
||||
# Diff with current manifest
|
||||
python3 skills/guardrail-orchestrator/scripts/manifest_diff.py diff $LATEST current
|
||||
```
|
||||
|
||||
**MODE: versions (e.g., "v001 v002")**
|
||||
```bash
|
||||
# Diff two specific versions
|
||||
python3 skills/guardrail-orchestrator/scripts/manifest_diff.py diff v001 v002
|
||||
```
|
||||
|
||||
**MODE: single (e.g., "v001")**
|
||||
```bash
|
||||
# Diff specific version with current
|
||||
python3 skills/guardrail-orchestrator/scripts/manifest_diff.py diff v001
|
||||
```
|
||||
|
||||
**MODE: changelog**
|
||||
```bash
|
||||
# Show all changelogs
|
||||
python3 skills/guardrail-orchestrator/scripts/manifest_diff.py changelog
|
||||
```
|
||||
|
||||
**JSON output**
|
||||
```bash
|
||||
# Add --json for programmatic use
|
||||
python3 skills/guardrail-orchestrator/scripts/manifest_diff.py diff v001 --json
|
||||
```
|
||||
|
||||
### Step 4: Display Results
|
||||
|
||||
The script outputs:
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════════════╗
|
||||
║ MANIFEST DIFF: v001 → v002 ║
|
||||
╠══════════════════════════════════════════════════════════════════════╣
|
||||
║ SUMMARY ║
|
||||
║ + Added: 3 ║
|
||||
║ ~ Modified: 2 ║
|
||||
║ - Removed: 1 ║
|
||||
║ = Unchanged: 5 ║
|
||||
╠══════════════════════════════════════════════════════════════════════╣
|
||||
║ BY TYPE ║
|
||||
║ pages: +1 ║
|
||||
║ components: +2 ~1 ║
|
||||
║ api_endpoints: ~1 -1 ║
|
||||
╠══════════════════════════════════════════════════════════════════════╣
|
||||
║ ➕ ADDED ║
|
||||
║ + 📄 Profile (app/profile/page.tsx) ║
|
||||
║ + 🧩 Button (app/components/Button.tsx) ║
|
||||
║ + 🧩 Modal (app/components/Modal.tsx) ║
|
||||
╠══════════════════════════════════════════════════════════════════════╣
|
||||
║ 📝 MODIFIED ║
|
||||
║ ~ 🧩 Header (app/components/Header.tsx) ║
|
||||
║ dependencies: [] → ['Button'] ║
|
||||
║ ~ 🔌 users (app/api/users/route.ts) ║
|
||||
║ status: PENDING → IMPLEMENTED ║
|
||||
╠══════════════════════════════════════════════════════════════════════╣
|
||||
║ ➖ REMOVED ║
|
||||
║ - 🔌 legacy (app/api/legacy/route.ts) ║
|
||||
╚══════════════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## USAGE EXAMPLES
|
||||
|
||||
```bash
|
||||
# Compare latest version with current manifest
|
||||
/workflow:diff
|
||||
|
||||
# Compare two specific versions
|
||||
/workflow:diff v001 v002
|
||||
|
||||
# Compare specific version with current
|
||||
/workflow:diff v003
|
||||
|
||||
# Show all version changelogs
|
||||
/workflow:diff --changelog
|
||||
|
||||
# Output as JSON
|
||||
/workflow:diff v001 --json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WHAT IT SHOWS
|
||||
|
||||
### Entity Changes
|
||||
- **Added**: New pages, components, API endpoints, etc.
|
||||
- **Modified**: Status changes, dependency updates, path changes
|
||||
- **Removed**: Deleted entities from manifest
|
||||
|
||||
### Entity Type Icons
|
||||
- 📄 page
|
||||
- 🧩 component
|
||||
- 🔌 api_endpoint
|
||||
- 📚 lib
|
||||
- 🪝 hook
|
||||
- 📝 type
|
||||
- ⚙️ config
|
||||
|
||||
### Change Details
|
||||
- Entity name and file path
|
||||
- Specific field changes with before/after values
|
||||
- Summary statistics by type
|
||||
|
||||
---
|
||||
|
||||
## CHANGELOG MODE
|
||||
|
||||
Show version history with changes:
|
||||
|
||||
```bash
|
||||
/workflow:diff --changelog
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════════════╗
|
||||
║ CHANGELOG: v001 ║
|
||||
╠══════════════════════════════════════════════════════════════════════╣
|
||||
║ Feature: User authentication ║
|
||||
║ Status: completed ║
|
||||
║ Started: 2025-01-15 10:30:00 ║
|
||||
║ Completed: 2025-01-15 14:45:00 ║
|
||||
╠══════════════════════════════════════════════════════════════════════╣
|
||||
║ CHANGES ║
|
||||
║ + Added page: Login ║
|
||||
║ + Added page: Register ║
|
||||
║ + Added component: AuthForm ║
|
||||
║ + Added api_endpoint: auth ║
|
||||
╚══════════════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## INTEGRATION
|
||||
|
||||
### Uses Version Snapshots
|
||||
|
||||
The diff tool uses snapshots created by version_manager.py:
|
||||
- `snapshot_before/manifest.json` - Manifest at version start
|
||||
- `snapshot_after/manifest.json` - Manifest at version completion
|
||||
|
||||
These are automatically created when:
|
||||
- `/workflow:spawn` initializes a new version
|
||||
- `/workflow:complete` marks a version as done
|
||||
|
||||
### Related Commands
|
||||
|
||||
- `/workflow:history` - List all workflow versions
|
||||
- `/workflow:status` - Show current workflow state
|
||||
- `/workflow:changelog <version>` - Alias for `--changelog`
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
description: Implement frontend tasks (Frontend agent)
|
||||
allowed-tools: Read, Write, Edit, Bash
|
||||
---
|
||||
|
||||
# Frontend Agent - Implementation Mode
|
||||
|
||||
🎨 **FRONTEND AGENT ACTIVATED**
|
||||
|
||||
Implement task: "$ARGUMENTS"
|
||||
|
||||
## CRITICAL RULES
|
||||
|
||||
You are now the **FRONTEND AGENT**.
|
||||
|
||||
✅ **ALLOWED**:
|
||||
- Read any file
|
||||
- Write new files (components, pages)
|
||||
- Edit existing UI files
|
||||
- Run Bash (build, lint, type-check)
|
||||
|
||||
✅ **ALLOWED FILES**:
|
||||
- `app/components/**/*`
|
||||
- `app/**/page.tsx`
|
||||
- `app/**/layout.tsx`
|
||||
- `app/globals.css`
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Load Task
|
||||
First, get the version-specific tasks directory:
|
||||
```bash
|
||||
TASKS_DIR=$(python3 skills/guardrail-orchestrator/scripts/version_manager.py tasks-dir)
|
||||
```
|
||||
|
||||
Read the task file: `$TASKS_DIR/$ARGUMENTS.yml`
|
||||
- If "$ARGUMENTS" is `--next`: find first task with `agent: frontend` and `status: pending`
|
||||
|
||||
### Step 2: Update Workflow State
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py task <task_id> in_progress
|
||||
```
|
||||
|
||||
### Step 3: Verify Prerequisites
|
||||
- Check entity is `APPROVED` in `project_manifest.json`
|
||||
- Check all `dependencies` tasks are `completed`
|
||||
- If blocked:
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py task <task_id> blocked
|
||||
```
|
||||
Stop and report blocker.
|
||||
|
||||
### Step 4: Implement
|
||||
For each file in `file_paths`:
|
||||
1. Read manifest entity specification
|
||||
2. Generate code matching spec exactly:
|
||||
- Props must match manifest
|
||||
- Types must match manifest
|
||||
- File path must match manifest
|
||||
3. Follow existing project patterns
|
||||
|
||||
### Step 5: Validate
|
||||
Run validations:
|
||||
```bash
|
||||
npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Step 6: Update Task Status
|
||||
Update the task file:
|
||||
```yaml
|
||||
status: review
|
||||
completed_at: <current timestamp>
|
||||
```
|
||||
|
||||
Update workflow state:
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py task <task_id> review
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py progress --tasks-impl <count>
|
||||
```
|
||||
|
||||
### Step 7: Report
|
||||
- List implemented files
|
||||
- Show validation results
|
||||
- Suggest: `/workflow:review $ARGUMENTS`
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
---
|
||||
description: Show workflow version history
|
||||
allowed-tools: Read, Bash
|
||||
---
|
||||
|
||||
# Workflow History
|
||||
|
||||
Display version history of all workflow sessions.
|
||||
|
||||
## Usage
|
||||
```
|
||||
/workflow:history # List all versions
|
||||
/workflow:history v001 # Show details for specific version
|
||||
/workflow:history --changelog # Show changelog for current version
|
||||
```
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. List All Versions (default)
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/version_manager.py history
|
||||
```
|
||||
|
||||
### 2. Show Version Details
|
||||
If "$ARGUMENTS" is a version (e.g., `v001`):
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/version_manager.py changelog $ARGUMENTS
|
||||
```
|
||||
|
||||
### 3. Display Format
|
||||
|
||||
**Version List**:
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════════════╗
|
||||
║ WORKFLOW VERSION HISTORY ║
|
||||
╠══════════════════════════════════════════════════════════════════════╣
|
||||
║ ✅ v003: Dashboard with analytics ║
|
||||
║ Started: 2025-01-16T16:00:00 | Tasks: 12 | Ops: 45 ║
|
||||
║ ────────────────────────────────────────────────────────────────── ║
|
||||
║ ✅ v002: Task filters and search ║
|
||||
║ Started: 2025-01-16T14:00:00 | Tasks: 8 | Ops: 28 ║
|
||||
║ ────────────────────────────────────────────────────────────────── ║
|
||||
║ ✅ v001: User authentication ║
|
||||
║ Started: 2025-01-16T10:00:00 | Tasks: 5 | Ops: 18 ║
|
||||
╚══════════════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
**Version Changelog**:
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════════════╗
|
||||
║ CHANGELOG: v001 ║
|
||||
╠══════════════════════════════════════════════════════════════════════╣
|
||||
║ Feature: User authentication ║
|
||||
║ Status: completed ║
|
||||
╠══════════════════════════════════════════════════════════════════════╣
|
||||
║ CREATED ║
|
||||
║ + [page] page_login ║
|
||||
║ app/login/page.tsx ║
|
||||
║ + [component] component_LoginForm ║
|
||||
║ app/components/LoginForm.tsx ║
|
||||
║ + [api] api_auth ║
|
||||
║ app/api/auth/route.ts ║
|
||||
║ UPDATED ║
|
||||
║ ~ [component] component_Header ║
|
||||
║ DELETED ║
|
||||
║ (none) ║
|
||||
╠══════════════════════════════════════════════════════════════════════╣
|
||||
║ SUMMARY ║
|
||||
║ Entities: +3 ~1 -0 ║
|
||||
║ Files: +4 ~2 -0 ║
|
||||
╚══════════════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
### 4. Show Task Sessions
|
||||
If `$ARGUMENTS` includes `--tasks`:
|
||||
List all task sessions for the version with their operations:
|
||||
|
||||
```
|
||||
Task Sessions for v001:
|
||||
─────────────────────────────────────────────────
|
||||
🎨 task_create_LoginPage (frontend)
|
||||
Status: completed | Duration: 5m 32s
|
||||
Operations:
|
||||
+ CREATE file: app/login/page.tsx
|
||||
~ UPDATE manifest: project_manifest.json
|
||||
Review: ✅ approved
|
||||
|
||||
⚙️ task_create_AuthAPI (backend)
|
||||
Status: completed | Duration: 8m 15s
|
||||
Operations:
|
||||
+ CREATE file: app/api/auth/route.ts
|
||||
+ CREATE file: app/lib/auth.ts
|
||||
Review: ✅ approved
|
||||
```
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
---
|
||||
description: Reject a workflow gate and request changes
|
||||
allowed-tools: Read, Write, Bash
|
||||
---
|
||||
|
||||
# Reject Workflow Gate
|
||||
|
||||
Reject gate with reason: "$ARGUMENTS"
|
||||
|
||||
## Usage
|
||||
```
|
||||
/workflow:reject design "Need more API endpoints for authentication"
|
||||
/workflow:reject implementation "Login form missing validation"
|
||||
```
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Parse Arguments
|
||||
Extract:
|
||||
- `gate`: First word (design | implementation)
|
||||
- `reason`: Remaining text in quotes
|
||||
|
||||
If invalid format:
|
||||
```
|
||||
❌ Usage: /workflow:reject <gate> "reason"
|
||||
|
||||
Examples:
|
||||
/workflow:reject design "Need user profile page"
|
||||
/workflow:reject implementation "Missing error handling"
|
||||
```
|
||||
|
||||
### 2. Check Workflow State
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py exists
|
||||
```
|
||||
|
||||
If no active workflow:
|
||||
```
|
||||
❌ No active workflow found.
|
||||
```
|
||||
|
||||
### 3. Verify Current Phase
|
||||
|
||||
**For design rejection**:
|
||||
- Current phase must be `AWAITING_DESIGN_APPROVAL`
|
||||
|
||||
**For implementation rejection**:
|
||||
- Current phase must be `AWAITING_IMPL_APPROVAL`
|
||||
|
||||
### 4. Execute Rejection
|
||||
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py reject <gate> "<reason>"
|
||||
```
|
||||
|
||||
### 5. Transition to Revision Phase
|
||||
|
||||
**If design rejected**:
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py transition DESIGN_REJECTED
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py transition DESIGNING
|
||||
```
|
||||
|
||||
**If implementation rejected**:
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py transition IMPL_REJECTED
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py transition IMPLEMENTING
|
||||
```
|
||||
|
||||
### 6. Report
|
||||
|
||||
**Design Rejected**:
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ ❌ DESIGN REJECTED ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Reason: <rejection_reason> ║
|
||||
║ ║
|
||||
║ The workflow has returned to the DESIGNING phase. ║
|
||||
║ Revision count: X ║
|
||||
║ ║
|
||||
║ Next steps: ║
|
||||
║ /workflow:design --revise Revise the design ║
|
||||
║ /workflow:resume Auto-revise and continue ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
**Implementation Rejected**:
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ ❌ IMPLEMENTATION REJECTED ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Reason: <rejection_reason> ║
|
||||
║ ║
|
||||
║ The workflow has returned to the IMPLEMENTING phase. ║
|
||||
║ Revision count: X ║
|
||||
║ ║
|
||||
║ Tasks requiring fixes will be marked as 'pending'. ║
|
||||
║ ║
|
||||
║ Next steps: ║
|
||||
║ /workflow:frontend --next Fix frontend tasks ║
|
||||
║ /workflow:backend --next Fix backend tasks ║
|
||||
║ /workflow:resume Auto-fix and continue ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
### 7. Update Related Tasks (Implementation Rejection)
|
||||
|
||||
If implementation was rejected, identify tasks related to the rejection reason and mark them as pending:
|
||||
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py task <affected_task_id> pending
|
||||
```
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
---
|
||||
description: Resume an interrupted workflow from saved state
|
||||
allowed-tools: Read, Write, Edit, Bash, AskUserQuestion, TodoWrite
|
||||
---
|
||||
|
||||
# Workflow Orchestrator - Resume
|
||||
|
||||
Resume a previously interrupted or paused workflow.
|
||||
|
||||
## EXECUTION PROTOCOL
|
||||
|
||||
### Step 1: Load Workflow State
|
||||
|
||||
Read `.workflow/current.yml`:
|
||||
- If not found: Report "No workflow to resume" and exit
|
||||
- If found: Load state and continue
|
||||
|
||||
### Step 2: Display Resume Summary
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ 🔄 RESUMING WORKFLOW ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Workflow ID: <id> ║
|
||||
║ Feature: <feature> ║
|
||||
║ Started: <started_at> ║
|
||||
║ Last Updated: <updated_at> ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ CURRENT STATE ║
|
||||
║ Phase: <current_phase> ║
|
||||
║ Resume Point: <resume_point.action> ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ PROGRESS ║
|
||||
║ Entities Designed: <progress.entities_designed> ║
|
||||
║ Tasks Created: <progress.tasks_created> ║
|
||||
║ Tasks Implemented: <progress.tasks_implemented> ║
|
||||
║ Tasks Reviewed: <progress.tasks_reviewed> ║
|
||||
║ Tasks Completed: <progress.tasks_completed> ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ LAST ERROR (if any): <last_error> ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
### Step 3: Confirm Resume
|
||||
|
||||
**Ask user**:
|
||||
- Option 1: "Continue - Resume from current point"
|
||||
- Option 2: "Restart Phase - Redo current phase from beginning"
|
||||
- Option 3: "Abort - Cancel workflow entirely"
|
||||
|
||||
### Step 4: Resume Based on Phase
|
||||
|
||||
**INITIALIZING**:
|
||||
→ Continue to DESIGNING phase
|
||||
|
||||
**DESIGNING**:
|
||||
→ Continue architect work
|
||||
→ Resume creating entities/tasks
|
||||
|
||||
**AWAITING_DESIGN_APPROVAL**:
|
||||
→ Present design summary again
|
||||
→ Ask for approval
|
||||
|
||||
**DESIGN_APPROVED**:
|
||||
→ Continue to IMPLEMENTING phase
|
||||
|
||||
**DESIGN_REJECTED**:
|
||||
→ Show rejection reason
|
||||
→ Return to DESIGNING with feedback
|
||||
|
||||
**IMPLEMENTING**:
|
||||
→ Find incomplete tasks
|
||||
→ Continue implementation from next pending task
|
||||
|
||||
**REVIEWING**:
|
||||
→ Find tasks awaiting review
|
||||
→ Continue review process
|
||||
|
||||
**SECURITY_REVIEW**:
|
||||
→ Continue security scanning
|
||||
→ Run: `python3 skills/guardrail-orchestrator/scripts/security_scan.py --project-dir . --severity HIGH`
|
||||
→ Run: `python3 skills/guardrail-orchestrator/scripts/validate_api_contract.py --project-dir .`
|
||||
→ If passed: Transition to AWAITING_IMPL_APPROVAL
|
||||
→ If critical issues: Return to IMPLEMENTING with security feedback
|
||||
|
||||
**AWAITING_IMPL_APPROVAL**:
|
||||
→ Present implementation summary again
|
||||
→ Ask for approval
|
||||
|
||||
**IMPL_APPROVED**:
|
||||
→ Continue to COMPLETING phase
|
||||
|
||||
**IMPL_REJECTED**:
|
||||
→ Show rejection reason
|
||||
→ Return to IMPLEMENTING with feedback
|
||||
|
||||
**COMPLETING**:
|
||||
→ Continue marking tasks complete
|
||||
|
||||
**PAUSED**:
|
||||
→ Resume from `resume_point.phase`
|
||||
|
||||
**FAILED**:
|
||||
→ Show error details
|
||||
→ Ask user how to proceed:
|
||||
- Retry failed operation
|
||||
- Skip and continue
|
||||
- Abort workflow
|
||||
|
||||
### Step 5: Continue Workflow
|
||||
|
||||
Execute remaining phases following `/workflow:spawn` protocol.
|
||||
|
||||
## TASK-LEVEL RESUME
|
||||
|
||||
If resuming during IMPLEMENTING phase:
|
||||
|
||||
1. **Identify incomplete tasks**:
|
||||
```yaml
|
||||
# Resume from first task not in 'completed' or 'approved'
|
||||
resume_task: tasks.pending[0] || tasks.in_progress[0] || tasks.review[0]
|
||||
```
|
||||
|
||||
2. **Skip completed work**:
|
||||
- Don't recreate files that exist and are valid
|
||||
- Don't re-run validations that passed
|
||||
|
||||
3. **Continue from failure point**:
|
||||
- If task failed mid-implementation, restart that task
|
||||
- If validation failed, show error and retry
|
||||
|
||||
## STATE RECOVERY
|
||||
|
||||
If `.workflow/current.yml` is corrupted:
|
||||
|
||||
1. **Check for backup**: `.workflow/current.yml.bak`
|
||||
2. **Attempt recovery from manifest**:
|
||||
- Read `project_manifest.json` for entity status
|
||||
- Scan version-specific tasks directory for task status
|
||||
- Reconstruct workflow state
|
||||
3. **If unrecoverable**:
|
||||
- Report error
|
||||
- Suggest starting fresh with `/workflow:spawn`
|
||||
|
||||
## ABORT WORKFLOW
|
||||
|
||||
If user chooses to abort:
|
||||
|
||||
1. **Confirm abort**:
|
||||
"This will cancel the workflow. Files already created will remain. Continue?"
|
||||
|
||||
2. **If confirmed**:
|
||||
- Archive state to `.workflow/history/<id>_aborted.yml`
|
||||
- Clear `.workflow/current.yml`
|
||||
- Report: "Workflow aborted. Created files remain in place."
|
||||
|
||||
3. **Cleanup options**:
|
||||
- Offer to rollback created files (if git available)
|
||||
- Offer to keep partial implementation
|
||||
|
|
@ -0,0 +1,526 @@
|
|||
---
|
||||
description: Review implementation (Reviewer agent)
|
||||
allowed-tools: Read, Bash
|
||||
---
|
||||
|
||||
# Reviewer Agent - Review Mode
|
||||
|
||||
**Input**: "$ARGUMENTS"
|
||||
|
||||
---
|
||||
|
||||
## CRITICAL ENFORCEMENT RULES
|
||||
|
||||
**YOU ARE IN READ-ONLY MODE. VIOLATIONS WILL BE BLOCKED.**
|
||||
|
||||
### MUST DO (Non-Negotiable)
|
||||
1. **MUST** run all validation checks (build, typecheck, lint, test, API contract)
|
||||
2. **MUST** verify every file in task's `file_paths` exists
|
||||
3. **MUST** read and analyze each implemented file
|
||||
4. **MUST** check against acceptance_criteria in task file
|
||||
5. **MUST** output structured review report (format below)
|
||||
6. **MUST** run workflow_manager.py to update task status
|
||||
|
||||
### CANNOT DO (Strictly Forbidden)
|
||||
1. **CANNOT** create files (Write tool blocked)
|
||||
2. **CANNOT** modify files (Edit tool blocked)
|
||||
3. **CANNOT** fix issues yourself - only report them
|
||||
4. **CANNOT** approve tasks with missing files
|
||||
5. **CANNOT** approve if ANY validation check fails
|
||||
6. **CANNOT** skip any validation check
|
||||
|
||||
### ALLOWED ACTIONS
|
||||
- Read any file
|
||||
- Run Bash commands (build, lint, test, typecheck, ls, cat, grep)
|
||||
- Output review reports
|
||||
- Update task status via workflow_manager.py
|
||||
|
||||
---
|
||||
|
||||
## VALIDATION CHECKS MATRIX
|
||||
|
||||
| Check | Command | Blocks Approval | When |
|
||||
|-------|---------|-----------------|------|
|
||||
| Build | `npm run build` | YES | Always |
|
||||
| TypeScript | `npx tsc --noEmit` | YES | Always |
|
||||
| Async/Await | `python3 verify_async.py` | YES | Always |
|
||||
| Lint | `npm run lint` | YES (if --strict) | --strict mode |
|
||||
| Unit Tests | `npm test -- --passWithNoTests` | YES (if --strict) | --strict mode |
|
||||
| API Contract | `python3 validate_api_contract.py` | YES | Always |
|
||||
| Security Scan | `python3 security_scan.py` | YES (critical) | Always |
|
||||
| Files Exist | `ls -la` each file | YES | Always |
|
||||
|
||||
**Note:** For comprehensive security audit, use `/workflow:security --full`
|
||||
|
||||
---
|
||||
|
||||
## ARGUMENT PARSING
|
||||
|
||||
```
|
||||
IF "$ARGUMENTS" contains "--auto":
|
||||
MODE = AUTO (batch review all tasks)
|
||||
STRICT = "$ARGUMENTS" contains "--strict"
|
||||
FULL = "$ARGUMENTS" contains "--full"
|
||||
ELSE IF "$ARGUMENTS" = "--next":
|
||||
MODE = SINGLE (next pending task)
|
||||
ELSE:
|
||||
MODE = SINGLE (specific task: "$ARGUMENTS")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MODE: AUTO REVIEW (--auto)
|
||||
|
||||
### Step A1: Get Active Version [MANDATORY]
|
||||
```bash
|
||||
VERSION_ID=$(python3 skills/guardrail-orchestrator/scripts/version_manager.py current)
|
||||
TASKS_DIR=".workflow/versions/$VERSION_ID/tasks"
|
||||
```
|
||||
|
||||
### Step A2: Run Global Validations [MANDATORY]
|
||||
|
||||
#### 2.1 Build Check
|
||||
```bash
|
||||
npm run build 2>&1
|
||||
BUILD_EXIT=$?
|
||||
echo "BUILD_EXIT=$BUILD_EXIT"
|
||||
```
|
||||
|
||||
#### 2.2 TypeScript Strict Check
|
||||
```bash
|
||||
npx tsc --noEmit 2>&1
|
||||
TS_EXIT=$?
|
||||
echo "TS_EXIT=$TS_EXIT"
|
||||
```
|
||||
|
||||
#### 2.3 Async/Await Check [MANDATORY]
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/verify_async.py --path . 2>&1
|
||||
ASYNC_EXIT=$?
|
||||
echo "ASYNC_EXIT=$ASYNC_EXIT"
|
||||
```
|
||||
|
||||
This catches runtime errors at build time:
|
||||
- `fetch()` without `await`
|
||||
- `.json()` without `await`
|
||||
- `Promise.all()` without `await`
|
||||
- Floating promises (unawaited async calls)
|
||||
|
||||
**Exit codes:**
|
||||
- 0 = PASS (no high-severity issues)
|
||||
- 1 = HIGH severity issues found (blocks approval)
|
||||
|
||||
#### 2.4 Lint Check (if --strict or --full)
|
||||
```bash
|
||||
npm run lint 2>&1
|
||||
LINT_EXIT=$?
|
||||
echo "LINT_EXIT=$LINT_EXIT"
|
||||
```
|
||||
|
||||
#### 2.5 Unit Tests (if --strict or --full)
|
||||
```bash
|
||||
npm test -- --passWithNoTests 2>&1
|
||||
TEST_EXIT=$?
|
||||
echo "TEST_EXIT=$TEST_EXIT"
|
||||
```
|
||||
|
||||
#### 2.6 API Contract Validation [MANDATORY]
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/validate_api_contract.py --project-dir . 2>&1
|
||||
API_EXIT=$?
|
||||
echo "API_EXIT=$API_EXIT"
|
||||
```
|
||||
|
||||
This validates:
|
||||
- Frontend API calls have matching backend endpoints
|
||||
- HTTP methods match (GET, POST, PUT, DELETE)
|
||||
- Request bodies are sent where expected
|
||||
- Response handling matches backend output
|
||||
|
||||
#### 2.7 Security Scan [MANDATORY]
|
||||
```bash
|
||||
# Run comprehensive security scanner
|
||||
python3 skills/guardrail-orchestrator/scripts/security_scan.py \
|
||||
--project-dir . \
|
||||
--severity HIGH
|
||||
SECURITY_EXIT=$?
|
||||
echo "SECURITY_EXIT=$SECURITY_EXIT"
|
||||
```
|
||||
|
||||
**Security scan checks:**
|
||||
- Hardcoded secrets (API keys, passwords, tokens)
|
||||
- SQL injection vulnerabilities
|
||||
- XSS risks (dangerouslySetInnerHTML, innerHTML)
|
||||
- Command injection patterns
|
||||
- Path traversal vulnerabilities
|
||||
- NoSQL injection risks
|
||||
- SSRF vulnerabilities
|
||||
- Prototype pollution
|
||||
- Insecure authentication patterns
|
||||
- CORS misconfigurations
|
||||
- Sensitive data exposure
|
||||
- Debug code in production
|
||||
|
||||
**Exit codes:**
|
||||
- 0 = PASS (no critical/high issues)
|
||||
- 1 = HIGH issues found (warning)
|
||||
- 2 = CRITICAL issues found (blocks approval)
|
||||
|
||||
**For full security audit, run:** `/workflow:security --full`
|
||||
|
||||
### Step A3: Gather All Tasks [MANDATORY]
|
||||
```bash
|
||||
ls $TASKS_DIR/*.yml 2>/dev/null
|
||||
```
|
||||
**MUST process ALL task files found**
|
||||
|
||||
### Step A4: Review Each Task [MANDATORY]
|
||||
|
||||
For EACH task file:
|
||||
|
||||
```bash
|
||||
# Extract file_paths from task
|
||||
TASK_FILES=$(grep -A 20 "file_paths:" "$TASK_FILE" | grep -E "^\s+-" | sed 's/.*- //')
|
||||
```
|
||||
|
||||
**Check each file exists**:
|
||||
```bash
|
||||
for file in $TASK_FILES; do
|
||||
if [ -f "$file" ]; then
|
||||
echo "EXISTS: $file"
|
||||
else
|
||||
echo "MISSING: $file"
|
||||
MISSING_COUNT=$((MISSING_COUNT + 1))
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
**Determine task verdict**:
|
||||
```
|
||||
IF all files exist
|
||||
AND BUILD_EXIT = 0
|
||||
AND TS_EXIT = 0
|
||||
AND ASYNC_EXIT = 0
|
||||
AND API_EXIT = 0
|
||||
AND SECURITY_EXIT != 2 (no critical security issues)
|
||||
AND (not --strict OR (LINT_EXIT = 0 AND TEST_EXIT = 0)):
|
||||
-> TASK_VERDICT = APPROVED
|
||||
ELSE:
|
||||
-> TASK_VERDICT = REJECTED
|
||||
-> Record reason (missing files / build failure / type error / async issue / API mismatch / security issue)
|
||||
```
|
||||
|
||||
**Security exit codes:**
|
||||
- 0 = PASS
|
||||
- 1 = HIGH issues (warning, doesn't block unless --strict)
|
||||
- 2 = CRITICAL issues (always blocks)
|
||||
|
||||
### Step A5: Batch Update [MANDATORY]
|
||||
|
||||
For APPROVED tasks:
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py task <task_id> approved
|
||||
```
|
||||
|
||||
For REJECTED tasks:
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py task <task_id> pending
|
||||
```
|
||||
|
||||
### Step A6: Auto Review Report [MANDATORY]
|
||||
|
||||
**MUST output this exact format**:
|
||||
```
|
||||
+======================================================================+
|
||||
| REVIEW COMPLETE |
|
||||
+======================================================================+
|
||||
| Version: $VERSION_ID |
|
||||
| Mode: AUTO [STRICT if --strict] [FULL if --full] |
|
||||
+======================================================================+
|
||||
| VALIDATION RESULTS |
|
||||
+----------------------------------------------------------------------+
|
||||
| Build: PASS (exit 0) / FAIL (exit $BUILD_EXIT) |
|
||||
| TypeScript: PASS (exit 0) / FAIL (exit $TS_EXIT) |
|
||||
| Async/Await: PASS / FAIL (X high, Y medium issues) |
|
||||
| Lint: PASS / FAIL / SKIPPED |
|
||||
| Tests: PASS / FAIL / SKIPPED |
|
||||
| API Contract: PASS / FAIL (X errors, Y warnings) |
|
||||
| Security: PASS / WARNING / CRITICAL |
|
||||
| (C:X H:X M:X L:X issues) |
|
||||
+======================================================================+
|
||||
| API CONTRACT DETAILS |
|
||||
+----------------------------------------------------------------------+
|
||||
| Frontend calls: X matched, Y unmatched |
|
||||
| Backend endpoints: X defined, Y unused |
|
||||
| Method mismatches: X |
|
||||
| Body mismatches: X |
|
||||
+======================================================================+
|
||||
| TASK RESULTS |
|
||||
+----------------------------------------------------------------------+
|
||||
| Total: X tasks |
|
||||
| Approved: X tasks |
|
||||
| Rejected: X tasks |
|
||||
| Skipped: X tasks (already completed) |
|
||||
+======================================================================+
|
||||
| APPROVED TASKS |
|
||||
| - task_create_Button |
|
||||
| - task_create_Form |
|
||||
+----------------------------------------------------------------------+
|
||||
| REJECTED TASKS |
|
||||
| - task_create_Modal |
|
||||
| Reason: Missing file app/components/Modal.tsx |
|
||||
| - task_update_API |
|
||||
| Reason: API contract error - endpoint not found |
|
||||
+======================================================================+
|
||||
| SECURITY WARNINGS |
|
||||
| - src/lib/api.ts:15 - Possible hardcoded API key |
|
||||
| - app/page.tsx:42 - dangerouslySetInnerHTML usage |
|
||||
+======================================================================+
|
||||
```
|
||||
|
||||
### Step A7: Next Steps [MANDATORY]
|
||||
|
||||
**IF all approved**:
|
||||
```
|
||||
All tasks approved.
|
||||
Next: Run `/workflow:approve implementation` to continue.
|
||||
```
|
||||
|
||||
**IF any rejected**:
|
||||
```
|
||||
Some tasks need fixes.
|
||||
|
||||
API Contract Issues:
|
||||
For frontend issues: Fix the API call URL or method
|
||||
For backend issues: Create or fix the API endpoint
|
||||
|
||||
For each rejected task, run:
|
||||
/workflow:frontend <task_id> (for frontend tasks)
|
||||
/workflow:backend <task_id> (for backend tasks)
|
||||
|
||||
Then re-run: /workflow:review --auto
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MODE: SINGLE TASK REVIEW (--next or task_id)
|
||||
|
||||
### Step S1: Get Task [MANDATORY]
|
||||
```bash
|
||||
VERSION_ID=$(python3 skills/guardrail-orchestrator/scripts/version_manager.py current)
|
||||
TASKS_DIR=".workflow/versions/$VERSION_ID/tasks"
|
||||
```
|
||||
|
||||
**IF --next**:
|
||||
```bash
|
||||
# Find first task with status: pending or status: implemented
|
||||
TASK_FILE=$(grep -l "status: pending\|status: implemented" $TASKS_DIR/*.yml 2>/dev/null | head -1)
|
||||
```
|
||||
|
||||
**IF specific task_id**:
|
||||
```bash
|
||||
TASK_FILE="$TASKS_DIR/$ARGUMENTS.yml"
|
||||
```
|
||||
|
||||
**BLOCK IF**: Task file does not exist -> Error: "Task not found: $ARGUMENTS"
|
||||
|
||||
### Step S2: Read Task Spec [MANDATORY]
|
||||
```bash
|
||||
cat "$TASK_FILE"
|
||||
```
|
||||
|
||||
Extract:
|
||||
- `id`: Task identifier
|
||||
- `file_paths`: List of files to verify
|
||||
- `acceptance_criteria`: List of requirements to check
|
||||
|
||||
### Step S3: Run All Validations [MANDATORY]
|
||||
|
||||
```bash
|
||||
# Build
|
||||
npm run build 2>&1
|
||||
BUILD_EXIT=$?
|
||||
|
||||
# TypeScript
|
||||
npx tsc --noEmit 2>&1
|
||||
TS_EXIT=$?
|
||||
|
||||
# API Contract
|
||||
python3 skills/guardrail-orchestrator/scripts/validate_api_contract.py --project-dir . 2>&1
|
||||
API_EXIT=$?
|
||||
```
|
||||
|
||||
**MUST capture and report all exit codes**
|
||||
|
||||
### Step S4: Verify Files Exist [MANDATORY]
|
||||
|
||||
For each path in `file_paths`:
|
||||
```bash
|
||||
ls -la "$path" 2>/dev/null && echo "EXISTS" || echo "MISSING"
|
||||
```
|
||||
|
||||
**Record**:
|
||||
- FILES_EXIST = true/false
|
||||
- MISSING_FILES = list of missing paths
|
||||
|
||||
### Step S5: Read and Analyze Files [MANDATORY]
|
||||
|
||||
For each EXISTING file:
|
||||
1. Read file content
|
||||
2. Check against acceptance_criteria:
|
||||
- [ ] File exports correct components/functions
|
||||
- [ ] Props/types match manifest spec
|
||||
- [ ] Code follows project patterns
|
||||
- [ ] No obvious bugs or issues
|
||||
3. Check API contract compliance:
|
||||
- [ ] Frontend calls use correct endpoints
|
||||
- [ ] HTTP methods are appropriate
|
||||
- [ ] Request bodies are properly structured
|
||||
- [ ] Response handling is correct
|
||||
|
||||
### Step S6: Determine Verdict [MANDATORY]
|
||||
|
||||
```
|
||||
IF BUILD_EXIT = 0
|
||||
AND TS_EXIT = 0
|
||||
AND API_EXIT = 0
|
||||
AND FILES_EXIST = true
|
||||
AND acceptance_criteria met:
|
||||
-> VERDICT = APPROVED
|
||||
ELSE:
|
||||
-> VERDICT = REQUEST_CHANGES
|
||||
-> Record all issues found
|
||||
```
|
||||
|
||||
### Step S7: Update Task Status [MANDATORY]
|
||||
|
||||
**IF APPROVED**:
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py task $TASK_ID approved
|
||||
```
|
||||
|
||||
**IF REQUEST_CHANGES**:
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py task $TASK_ID pending
|
||||
```
|
||||
|
||||
### Step S8: Output Review Report [MANDATORY]
|
||||
|
||||
**MUST output this exact format**:
|
||||
```
|
||||
+======================================================================+
|
||||
| TASK REVIEW |
|
||||
+======================================================================+
|
||||
| Task: $TASK_ID |
|
||||
| Version: $VERSION_ID |
|
||||
+======================================================================+
|
||||
| VALIDATION CHECKS |
|
||||
+----------------------------------------------------------------------+
|
||||
| Build: PASS / FAIL |
|
||||
| TypeScript: PASS / FAIL |
|
||||
| API Contract: PASS / FAIL |
|
||||
| Files exist: PASS / FAIL (X/Y files) |
|
||||
| Acceptance criteria: PASS / PARTIAL / FAIL |
|
||||
| Code quality: PASS / ISSUES |
|
||||
+======================================================================+
|
||||
| API CONTRACT STATUS |
|
||||
+----------------------------------------------------------------------+
|
||||
| Endpoint calls: X found, Y matched |
|
||||
| Method correctness: PASS / X mismatches |
|
||||
| Request bodies: PASS / X issues |
|
||||
| Response handling: PASS / ISSUES |
|
||||
+======================================================================+
|
||||
| VERDICT: APPROVED / REQUEST_CHANGES |
|
||||
+======================================================================+
|
||||
| [If REQUEST_CHANGES, list all issues:] |
|
||||
| 1. Missing file: app/components/Button.tsx |
|
||||
| 2. TypeScript error: Type 'string' not assignable to 'number' |
|
||||
| 3. API contract: POST /api/users called but endpoint expects GET |
|
||||
| 4. API contract: Frontend sends body but backend ignores it |
|
||||
| 5. Acceptance criterion not met: "Must support dark mode" |
|
||||
+======================================================================+
|
||||
```
|
||||
|
||||
### Step S9: Next Steps [MANDATORY]
|
||||
|
||||
**IF APPROVED**:
|
||||
```
|
||||
Task approved.
|
||||
Next: Run `/workflow:complete $TASK_ID` to mark as done.
|
||||
Or run `/workflow:review --next` to review next task.
|
||||
```
|
||||
|
||||
**IF REQUEST_CHANGES**:
|
||||
```
|
||||
Changes requested.
|
||||
|
||||
Issues to fix:
|
||||
[List specific issues with file locations]
|
||||
|
||||
For API contract issues:
|
||||
- If frontend issue: Fix the fetch/axios call in [file:line]
|
||||
- If backend issue: Update the API route handler in [file]
|
||||
|
||||
Fix issues and re-run:
|
||||
/workflow:frontend $TASK_ID (for frontend tasks)
|
||||
/workflow:backend $TASK_ID (for backend tasks)
|
||||
Then: /workflow:review $TASK_ID
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## USAGE EXAMPLES
|
||||
|
||||
```bash
|
||||
# Review specific task
|
||||
/workflow:review task_create_Button
|
||||
|
||||
# Review next pending task
|
||||
/workflow:review --next
|
||||
|
||||
# Auto-review all tasks (standard - build + types + API)
|
||||
/workflow:review --auto
|
||||
|
||||
# Auto-review all tasks (strict - includes lint + tests)
|
||||
/workflow:review --auto --strict
|
||||
|
||||
# Full review with all checks
|
||||
/workflow:review --auto --full
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API CONTRACT VALIDATION DETAILS
|
||||
|
||||
The API contract validator checks:
|
||||
|
||||
### Frontend Analysis
|
||||
- **fetch()** calls with `/api/` paths
|
||||
- **axios** requests (get, post, put, delete)
|
||||
- **useSWR** data fetching hooks
|
||||
- **Custom API clients** (api.get, api.post, etc.)
|
||||
|
||||
### Backend Analysis
|
||||
- **Next.js App Router**: `app/api/*/route.ts` exports (GET, POST, PUT, DELETE)
|
||||
- **Next.js Pages Router**: `pages/api/*.ts` with req.method checks
|
||||
- **Express-style**: router.get/post/etc patterns
|
||||
|
||||
### Validation Rules
|
||||
1. **Endpoint Existence**: Every frontend call must have a matching backend route
|
||||
2. **Method Match**: GET calls must hit GET endpoints, POST to POST, etc.
|
||||
3. **Body Alignment**: POST/PUT calls should send bodies, GET should not
|
||||
4. **Unused Endpoints**: Backend routes not called by frontend (warnings)
|
||||
|
||||
---
|
||||
|
||||
## ENFORCEMENT CHECKLIST
|
||||
|
||||
Before completing this command, verify:
|
||||
- [ ] Build command executed and exit code captured
|
||||
- [ ] TypeScript check executed and exit code captured
|
||||
- [ ] API contract validation executed and exit code captured
|
||||
- [ ] All file_paths verified with ls command
|
||||
- [ ] Security scan completed
|
||||
- [ ] Structured review report output (exact format above)
|
||||
- [ ] Task status updated via workflow_manager.py
|
||||
- [ ] Next steps clearly stated
|
||||
|
|
@ -0,0 +1,342 @@
|
|||
---
|
||||
description: Run comprehensive security audit (Security Reviewer agent)
|
||||
allowed-tools: Read, Bash, Grep, Task
|
||||
---
|
||||
|
||||
# Security Reviewer Agent - Security Audit Mode
|
||||
|
||||
**Input**: "$ARGUMENTS"
|
||||
|
||||
---
|
||||
|
||||
## CRITICAL CONSTRAINTS
|
||||
|
||||
**YOU ARE IN READ-ONLY MODE FOR ANALYSIS.**
|
||||
|
||||
### MUST DO (Non-Negotiable)
|
||||
1. **MUST** run automated security scanner
|
||||
2. **MUST** analyze all CRITICAL and HIGH findings
|
||||
3. **MUST** check dependency vulnerabilities
|
||||
4. **MUST** review security configurations
|
||||
5. **MUST** output structured security report
|
||||
6. **MUST** provide remediation guidance
|
||||
|
||||
### CANNOT DO (Strictly Forbidden)
|
||||
1. **CANNOT** modify source files
|
||||
2. **CANNOT** fix issues directly
|
||||
3. **CANNOT** approve with CRITICAL issues
|
||||
4. **CANNOT** skip any security category
|
||||
|
||||
---
|
||||
|
||||
## ARGUMENT PARSING
|
||||
|
||||
```
|
||||
IF "$ARGUMENTS" contains "--quick":
|
||||
MODE = QUICK (scanner only)
|
||||
ELSE IF "$ARGUMENTS" contains "--full":
|
||||
MODE = FULL (scanner + deep analysis + deps + config)
|
||||
ELSE:
|
||||
MODE = STANDARD (scanner + deps)
|
||||
|
||||
SEVERITY = extract from --severity [critical|high|medium|low]
|
||||
OUTPUT = extract from --json (JSON output) or text
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## EXECUTION FLOW
|
||||
|
||||
### Step 1: Run Automated Security Scanner [MANDATORY]
|
||||
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/security_scan.py \
|
||||
--project-dir . \
|
||||
--severity ${SEVERITY:-LOW} \
|
||||
${OUTPUT:+--json}
|
||||
```
|
||||
|
||||
**Capture output and exit code:**
|
||||
```bash
|
||||
SCAN_EXIT=$?
|
||||
echo "SCAN_EXIT=$SCAN_EXIT"
|
||||
```
|
||||
|
||||
**Exit codes:**
|
||||
- 0 = PASS (no critical/high issues)
|
||||
- 1 = HIGH issues found
|
||||
- 2 = CRITICAL issues found
|
||||
|
||||
### Step 2: Dependency Audit [MANDATORY unless --quick]
|
||||
|
||||
```bash
|
||||
echo "=== Dependency Audit ==="
|
||||
npm audit --json 2>/dev/null || echo '{"vulnerabilities":{}}'
|
||||
```
|
||||
|
||||
**Parse npm audit results:**
|
||||
- Count critical, high, moderate, low vulnerabilities
|
||||
- List affected packages and versions
|
||||
- Note if fixes available (`npm audit fix`)
|
||||
|
||||
### Step 3: Deep Analysis [FULL mode only]
|
||||
|
||||
For each CRITICAL/HIGH finding from scanner:
|
||||
|
||||
#### 3.1 Data Flow Tracing
|
||||
Use Task agent with security-engineer subagent:
|
||||
```
|
||||
Analyze data flow for vulnerability at [file:line].
|
||||
Trace user input from source to sink.
|
||||
Identify all potential attack vectors.
|
||||
Assess exploitability and impact.
|
||||
```
|
||||
|
||||
#### 3.2 Attack Vector Analysis
|
||||
For each vulnerability type:
|
||||
- SQL Injection → Check if input reaches query without sanitization
|
||||
- XSS → Check if input reaches DOM without encoding
|
||||
- Command Injection → Check if input reaches shell without escaping
|
||||
- Path Traversal → Check if input reaches file system without validation
|
||||
|
||||
### Step 4: Configuration Review [FULL mode only]
|
||||
|
||||
#### 4.1 CORS Configuration
|
||||
```bash
|
||||
grep -rn "cors\|Access-Control" app/ src/ pages/ --include="*.ts" --include="*.tsx" --include="*.js"
|
||||
```
|
||||
|
||||
Check for:
|
||||
- Wildcard origins (`*`)
|
||||
- Credentials with permissive origins
|
||||
- Missing CORS on sensitive endpoints
|
||||
|
||||
#### 4.2 Security Headers
|
||||
```bash
|
||||
grep -rn "helmet\|Content-Security-Policy\|X-Frame-Options\|X-XSS-Protection" . --include="*.ts" --include="*.js"
|
||||
```
|
||||
|
||||
Check for:
|
||||
- Helmet middleware usage
|
||||
- CSP configuration
|
||||
- X-Frame-Options
|
||||
- X-Content-Type-Options
|
||||
|
||||
#### 4.3 Authentication Configuration
|
||||
```bash
|
||||
grep -rn "jwt\|session\|auth\|cookie" app/ src/ pages/ --include="*.ts" --include="*.tsx"
|
||||
```
|
||||
|
||||
Check for:
|
||||
- JWT algorithm (avoid 'none', prefer RS256)
|
||||
- Session configuration
|
||||
- Cookie flags (httpOnly, secure, sameSite)
|
||||
|
||||
#### 4.4 Environment Variables
|
||||
```bash
|
||||
# Check .env files are gitignored
|
||||
cat .gitignore 2>/dev/null | grep -E "\.env"
|
||||
|
||||
# Check for env var usage
|
||||
grep -rn "process\.env\." app/ src/ --include="*.ts" --include="*.tsx" | head -20
|
||||
```
|
||||
|
||||
### Step 5: Manual Review Checklist [FULL mode only]
|
||||
|
||||
Read each file modified in current workflow and verify:
|
||||
|
||||
**Input Validation**
|
||||
- [ ] All user inputs validated
|
||||
- [ ] Type checking enforced
|
||||
- [ ] Length limits applied
|
||||
- [ ] Format validation (email, URL, etc.)
|
||||
|
||||
**Output Encoding**
|
||||
- [ ] HTML encoding for DOM insertion
|
||||
- [ ] URL encoding for URLs
|
||||
- [ ] JSON encoding for API responses
|
||||
|
||||
**Database Security**
|
||||
- [ ] Parameterized queries used
|
||||
- [ ] No string concatenation in queries
|
||||
- [ ] Proper ORM usage
|
||||
|
||||
**Authentication/Authorization**
|
||||
- [ ] Auth checks on protected routes
|
||||
- [ ] Role-based access control
|
||||
- [ ] Session validation
|
||||
|
||||
**Error Handling**
|
||||
- [ ] Generic error messages to users
|
||||
- [ ] No stack traces in production
|
||||
- [ ] No sensitive data in logs
|
||||
|
||||
### Step 6: Generate Security Report [MANDATORY]
|
||||
|
||||
**MUST output this exact format:**
|
||||
|
||||
```
|
||||
+======================================================================+
|
||||
| SECURITY AUDIT REPORT |
|
||||
+======================================================================+
|
||||
| Mode: QUICK / STANDARD / FULL |
|
||||
| Date: [current date] |
|
||||
| Project: [project name from package.json] |
|
||||
+======================================================================+
|
||||
| RISK ASSESSMENT |
|
||||
+----------------------------------------------------------------------+
|
||||
| Overall Risk: CRITICAL / HIGH / MEDIUM / LOW / PASS |
|
||||
| |
|
||||
| Static Analysis: X issues (C:X H:X M:X L:X) |
|
||||
| Dependencies: X vulnerabilities |
|
||||
| Configuration: X concerns |
|
||||
+======================================================================+
|
||||
| CRITICAL ISSUES (Immediate Action Required) |
|
||||
+----------------------------------------------------------------------+
|
||||
| [1] [CATEGORY] Title |
|
||||
| Location: file:line |
|
||||
| CWE: CWE-XXX |
|
||||
| OWASP: A0X:2021-Category |
|
||||
| Evidence: [code snippet] |
|
||||
| Impact: [description of potential attack] |
|
||||
| Fix: [specific remediation steps] |
|
||||
| |
|
||||
| [2] ... |
|
||||
+======================================================================+
|
||||
| HIGH ISSUES (Fix Before Production) |
|
||||
+----------------------------------------------------------------------+
|
||||
| [3] ... |
|
||||
+======================================================================+
|
||||
| MEDIUM ISSUES (Should Fix) |
|
||||
+----------------------------------------------------------------------+
|
||||
| [4] ... |
|
||||
+======================================================================+
|
||||
| DEPENDENCY VULNERABILITIES |
|
||||
+----------------------------------------------------------------------+
|
||||
| Package Version Severity Fix Available |
|
||||
| lodash 4.17.20 HIGH npm audit fix |
|
||||
| axios 0.21.0 MEDIUM npm audit fix |
|
||||
+======================================================================+
|
||||
| CONFIGURATION CONCERNS |
|
||||
+----------------------------------------------------------------------+
|
||||
| - CORS: Wildcard origin detected in src/middleware.ts |
|
||||
| - Session: Missing httpOnly flag on auth cookie |
|
||||
| - Headers: No CSP header configured |
|
||||
+======================================================================+
|
||||
| REMEDIATION PRIORITY |
|
||||
+----------------------------------------------------------------------+
|
||||
| 1. [CRITICAL] Rotate exposed API key in src/lib/api.ts |
|
||||
| 2. [CRITICAL] Fix SQL injection in app/api/users/route.ts |
|
||||
| 3. [HIGH] Update lodash to 4.17.21 |
|
||||
| 4. [HIGH] Add input validation to user registration |
|
||||
| 5. [MEDIUM] Configure CSP headers |
|
||||
+======================================================================+
|
||||
| VERDICT |
|
||||
+----------------------------------------------------------------------+
|
||||
| FAIL - X critical issues must be fixed before deployment |
|
||||
| or |
|
||||
| PASS - No blocking security issues found |
|
||||
+======================================================================+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## VERDICT DETERMINATION
|
||||
|
||||
### FAIL Conditions
|
||||
- Any CRITICAL issue found
|
||||
- 3+ HIGH issues found
|
||||
- Critical npm vulnerabilities without fix
|
||||
- Exposed secrets or credentials
|
||||
|
||||
### PASS WITH WARNINGS
|
||||
- Only MEDIUM/LOW issues
|
||||
- All HIGH issues have accepted risk
|
||||
- Dependencies have fixes available
|
||||
|
||||
### PASS
|
||||
- No CRITICAL/HIGH issues
|
||||
- Dependencies up to date
|
||||
- Configurations reviewed
|
||||
|
||||
---
|
||||
|
||||
## POST-AUDIT ACTIONS
|
||||
|
||||
### If FAIL:
|
||||
```
|
||||
SECURITY AUDIT FAILED
|
||||
|
||||
Blocking issues must be fixed:
|
||||
1. [List critical issues]
|
||||
|
||||
For each issue:
|
||||
/workflow:frontend <task_id> - if frontend issue
|
||||
/workflow:backend <task_id> - if backend issue
|
||||
|
||||
Then re-run: /workflow:security
|
||||
```
|
||||
|
||||
### If PASS:
|
||||
```
|
||||
SECURITY AUDIT PASSED
|
||||
|
||||
Proceed with: /workflow:review --auto
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## USAGE EXAMPLES
|
||||
|
||||
```bash
|
||||
# Quick scan (automated scanner only)
|
||||
/workflow:security --quick
|
||||
|
||||
# Standard scan (scanner + dependencies)
|
||||
/workflow:security
|
||||
|
||||
# Full audit (all checks)
|
||||
/workflow:security --full
|
||||
|
||||
# Filter by severity
|
||||
/workflow:security --severity high
|
||||
|
||||
# JSON output for CI/CD
|
||||
/workflow:security --json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## INTEGRATION WITH CI/CD
|
||||
|
||||
### Pre-commit Hook
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/security_scan.py \
|
||||
--project-dir . --severity HIGH --strict
|
||||
```
|
||||
|
||||
### GitHub Actions
|
||||
```yaml
|
||||
- name: Security Scan
|
||||
run: |
|
||||
python3 skills/guardrail-orchestrator/scripts/security_scan.py \
|
||||
--project-dir . --json > security-report.json
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Security issues found!"
|
||||
cat security-report.json
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ENFORCEMENT CHECKLIST
|
||||
|
||||
Before completing this command, verify:
|
||||
- [ ] Automated scanner executed
|
||||
- [ ] All categories analyzed
|
||||
- [ ] Dependencies audited (unless --quick)
|
||||
- [ ] Structured report output
|
||||
- [ ] Remediation guidance provided
|
||||
- [ ] Clear verdict stated
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,119 @@
|
|||
---
|
||||
description: Show workflow status and task summary
|
||||
allowed-tools: Read, Bash
|
||||
---
|
||||
|
||||
# Workflow Status
|
||||
|
||||
Display current workflow status and task breakdown.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Check Active Workflow
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/workflow_manager.py status
|
||||
```
|
||||
|
||||
If active workflow exists, display workflow state.
|
||||
If no workflow, continue with manual task scan.
|
||||
|
||||
### 2. Read Project Manifest
|
||||
Check `project_manifest.json` for:
|
||||
- Current phase
|
||||
- Entity counts by status
|
||||
|
||||
### 3. Scan Tasks
|
||||
Get the version-specific tasks directory:
|
||||
```bash
|
||||
TASKS_DIR=$(python3 skills/guardrail-orchestrator/scripts/version_manager.py tasks-dir)
|
||||
```
|
||||
|
||||
Read all `$TASKS_DIR/*.yml` files and count by:
|
||||
- Status (pending, in_progress, review, approved, completed, blocked)
|
||||
- Agent (frontend, backend, reviewer)
|
||||
- Type (create, update, delete, review)
|
||||
|
||||
### 4. Display Summary
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ WORKFLOW STATUS ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Active Workflow: <workflow_id> | None ║
|
||||
║ Feature: <feature_name> ║
|
||||
║ Phase: <current_phase> ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ APPROVAL GATES ║
|
||||
║ 🛑 Design: <pending|approved|rejected> ║
|
||||
║ 🛑 Implementation: <pending|approved|rejected> ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ TASKS BY STATUS ║
|
||||
║ ⏳ Pending: X ║
|
||||
║ 🔄 In Progress: X ║
|
||||
║ 🔍 Review: X ║
|
||||
║ ✅ Approved: X ║
|
||||
║ ✓ Completed: X ║
|
||||
║ 🚫 Blocked: X ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ TASKS BY AGENT ║
|
||||
║ 🎨 Frontend: X pending, X completed ║
|
||||
║ ⚙️ Backend: X pending, X completed ║
|
||||
║ 🔍 Reviewer: X pending ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ NEXT ACTIONS ║
|
||||
║ /workflow:frontend --next (X tasks available) ║
|
||||
║ /workflow:backend --next (X tasks available) ║
|
||||
║ /workflow:review --next (X tasks to review) ║
|
||||
║ /workflow:resume (continue workflow) ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
### 5. Show Design Visualization
|
||||
**If in DESIGNING or AWAITING_DESIGN_APPROVAL phase**, display visual design:
|
||||
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/visualize_design.py --manifest project_manifest.json
|
||||
```
|
||||
|
||||
This shows:
|
||||
- 📱 Page flow diagram
|
||||
- 📄 Page details with components
|
||||
- 🧩 Component hierarchy
|
||||
- 🔌 API endpoints
|
||||
- 🔄 Data flow architecture
|
||||
|
||||
### 5b. Show Implementation Visualization
|
||||
**If in REVIEWING, SECURITY_REVIEW, or AWAITING_IMPL_APPROVAL phase**, display what was built:
|
||||
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/visualize_implementation.py --manifest project_manifest.json
|
||||
```
|
||||
|
||||
This shows:
|
||||
- 📱 Page structure with routes
|
||||
- 🧩 Component hierarchy and relationships
|
||||
- 🔌 API endpoints with HTTP methods
|
||||
- 📊 Implementation statistics (lines, hooks, types)
|
||||
- 🌳 Component tree view
|
||||
|
||||
### 6. List Pending Tasks
|
||||
Show table of tasks ready to work on:
|
||||
|
||||
| Task ID | Type | Agent | Priority | Dependencies |
|
||||
|---------|------|-------|----------|--------------|
|
||||
|
||||
### 7. Show Approval Instructions
|
||||
|
||||
**If AWAITING_DESIGN_APPROVAL**:
|
||||
```
|
||||
🛑 Design approval required. Review the entities and tasks, then:
|
||||
- Approve: /workflow:approve design
|
||||
- Reject: /workflow:reject design "reason"
|
||||
```
|
||||
|
||||
**If AWAITING_IMPL_APPROVAL**:
|
||||
```
|
||||
🛑 Implementation approval required. Review the code, then:
|
||||
- Approve: /workflow:approve implementation
|
||||
- Reject: /workflow:reject implementation "reason"
|
||||
```
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
api_key: pk_user_8d080a1a699dc2a1769ca99ded0ca39fa80324b8713cf55ea7fecc1c372379a6
|
||||
project_id: ""
|
||||
repo_id: ""
|
||||
app_id: cmjb04ana0001qp0tijyy9emq
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_bash.py\" --command \"$TOOL_INPUT_COMMAND\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Task",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_workflow.py\" --operation task --input \"$TOOL_INPUT\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_workflow.py\" --operation write --file \"$TOOL_INPUT_FILE_PATH\""
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_write.py\" --manifest \"$CLAUDE_PROJECT_DIR/project_manifest.json\" --file \"$TOOL_INPUT_FILE_PATH\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Edit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_workflow.py\" --operation edit --file \"$TOOL_INPUT_FILE_PATH\""
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_write.py\" --manifest \"$CLAUDE_PROJECT_DIR/project_manifest.json\" --file \"$TOOL_INPUT_FILE_PATH\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "MultiEdit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_workflow.py\" --operation edit --file \"$TOOL_INPUT_FILE_PATH\""
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_write.py\" --manifest \"$CLAUDE_PROJECT_DIR/project_manifest.json\" --file \"$TOOL_INPUT_FILE_PATH\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "NotebookEdit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_workflow.py\" --operation edit --file \"$TOOL_INPUT_NOTEBOOK_PATH\""
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_write.py\" --manifest \"$CLAUDE_PROJECT_DIR/project_manifest.json\" --file \"$TOOL_INPUT_NOTEBOOK_PATH\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "mcp__serena__create_text_file",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_workflow.py\" --operation write --input \"$TOOL_INPUT\""
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_write.py\" --manifest \"$CLAUDE_PROJECT_DIR/project_manifest.json\" --file \"$TOOL_INPUT_FILE_PATH\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "mcp__serena__replace_content",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_workflow.py\" --operation edit --input \"$TOOL_INPUT\""
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_write.py\" --manifest \"$CLAUDE_PROJECT_DIR/project_manifest.json\" --file \"$TOOL_INPUT_FILE_PATH\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "mcp__serena__replace_symbol_body",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_workflow.py\" --operation edit --input \"$TOOL_INPUT\""
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_write.py\" --manifest \"$CLAUDE_PROJECT_DIR/project_manifest.json\" --file \"$TOOL_INPUT_FILE_PATH\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "mcp__morphllm-fast-apply__write_file",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_workflow.py\" --operation write --input \"$TOOL_INPUT\""
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_write.py\" --manifest \"$CLAUDE_PROJECT_DIR/project_manifest.json\" --file \"$TOOL_INPUT_FILE_PATH\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "mcp__morphllm-fast-apply__tiny_edit_file",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_workflow.py\" --operation edit --input \"$TOOL_INPUT\""
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_write.py\" --manifest \"$CLAUDE_PROJECT_DIR/project_manifest.json\" --file \"$TOOL_INPUT_FILE_PATH\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "mcp__filesystem__write_file",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_workflow.py\" --operation write --input \"$TOOL_INPUT\""
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_write.py\" --manifest \"$CLAUDE_PROJECT_DIR/project_manifest.json\" --file \"$TOOL_INPUT_PATH\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "mcp__filesystem__edit_file",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_workflow.py\" --operation edit --input \"$TOOL_INPUT\""
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_write.py\" --manifest \"$CLAUDE_PROJECT_DIR/project_manifest.json\" --file \"$TOOL_INPUT_PATH\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "mcp__filesystem__create_directory",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_workflow.py\" --operation write --input \"$TOOL_INPUT\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "mcp__filesystem__move_file",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_workflow.py\" --operation write --input \"$TOOL_INPUT\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/post_write.py\" --manifest \"$CLAUDE_PROJECT_DIR/project_manifest.json\" --file \"$TOOL_INPUT_FILE_PATH\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Edit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/post_write.py\" --manifest \"$CLAUDE_PROJECT_DIR/project_manifest.json\" --file \"$TOOL_INPUT_FILE_PATH\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Task",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "echo '🔄 Agent task completed. Verify outputs before proceeding.'"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"matcher": "",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/workflow_manager.py\" status 2>/dev/null || echo '🛡️ Session complete (no active workflow)'"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"eureka-docs": {
|
||||
"command": "npx",
|
||||
"args": ["eureka-docs-server"],
|
||||
"env": {}
|
||||
},
|
||||
"eureka-imagen": {
|
||||
"command": "npx",
|
||||
"args": ["eureka-imagen-server"],
|
||||
"env": {
|
||||
"IMAGEROUTER_API_KEY": "${IMAGEROUTER_API_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
# Documentation Writer Agent
|
||||
# Specialized agent for generating dual-audience documentation
|
||||
|
||||
name: doc-writer
|
||||
role: Documentation Specialist
|
||||
description: |
|
||||
Expert in creating comprehensive documentation that serves both technical
|
||||
and non-technical audiences. Specializes in translating complex technical
|
||||
concepts into accessible language while maintaining technical accuracy.
|
||||
|
||||
capabilities:
|
||||
- Analyze project structure and extract key information
|
||||
- Generate visual ASCII diagrams for architecture
|
||||
- Write plain-language descriptions of technical features
|
||||
- Create technical reference documentation
|
||||
- Build glossaries for technical terms
|
||||
- Structure documentation for multiple audience levels
|
||||
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
|
||||
blocked_tools:
|
||||
- Task # Should not spawn sub-agents
|
||||
|
||||
allowed_files:
|
||||
- "docs/**/*"
|
||||
- "*.md"
|
||||
- "package.json"
|
||||
- "project_manifest.json"
|
||||
- "tsconfig.json"
|
||||
- "requirements.txt"
|
||||
- "pyproject.toml"
|
||||
- "Cargo.toml"
|
||||
- "go.mod"
|
||||
|
||||
responsibilities:
|
||||
- Analyze source code to understand functionality
|
||||
- Extract API endpoints and document them
|
||||
- Document components with props and usage
|
||||
- Create ER diagrams for data models
|
||||
- Write executive summaries for stakeholders
|
||||
- Build glossaries for technical terms
|
||||
- Generate quick reference cards
|
||||
|
||||
outputs:
|
||||
- PROJECT_DOCUMENTATION.md (main documentation)
|
||||
- QUICK_REFERENCE.md (one-page summary)
|
||||
- API_REFERENCE.md (detailed API docs)
|
||||
- COMPONENTS.md (component catalog)
|
||||
- GLOSSARY.md (term definitions)
|
||||
|
||||
cannot_do:
|
||||
- Modify source code
|
||||
- Change project configuration
|
||||
- Run tests or builds
|
||||
- Deploy or publish
|
||||
|
||||
writing_principles:
|
||||
non_technical:
|
||||
- Lead with "What" and "Why", not "How"
|
||||
- Use analogies and real-world comparisons
|
||||
- Avoid acronyms; spell them out first time
|
||||
- Use bullet points over paragraphs
|
||||
- Include visual diagrams
|
||||
- Focus on value and outcomes
|
||||
|
||||
technical:
|
||||
- Include in collapsible <details> sections
|
||||
- Provide code examples with syntax highlighting
|
||||
- Reference file paths and line numbers
|
||||
- Include type definitions and interfaces
|
||||
- Link to source files
|
||||
- Document edge cases and error handling
|
||||
|
||||
documentation_sections:
|
||||
executive_summary:
|
||||
audience: everyone
|
||||
purpose: Project purpose, value proposition, key capabilities
|
||||
format: Plain English, no jargon
|
||||
|
||||
architecture_overview:
|
||||
audience: everyone
|
||||
purpose: Visual system understanding
|
||||
format: ASCII diagrams, technology tables
|
||||
|
||||
getting_started:
|
||||
audience: semi-technical
|
||||
purpose: Quick onboarding
|
||||
format: Step-by-step with explanations
|
||||
|
||||
feature_guide:
|
||||
audience: non-technical
|
||||
purpose: Feature documentation
|
||||
format: What/Why/How (simplified)
|
||||
|
||||
api_reference:
|
||||
audience: developers
|
||||
purpose: API documentation
|
||||
format: Endpoints, schemas, examples
|
||||
|
||||
component_catalog:
|
||||
audience: developers
|
||||
purpose: UI component documentation
|
||||
format: Props, events, usage examples
|
||||
|
||||
data_models:
|
||||
audience: both
|
||||
purpose: Data structure documentation
|
||||
format: ER diagrams + plain descriptions
|
||||
|
||||
glossary:
|
||||
audience: non-technical
|
||||
purpose: Term definitions
|
||||
format: Term -> Plain English definition
|
||||
|
||||
ascii_diagram_templates:
|
||||
system_architecture: |
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ [System Name] │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ [Layer] │───▶│ [Layer] │───▶│ [Layer] │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
entity_relationship: |
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ [Entity] │ │ [Entity] │
|
||||
├──────────────┤ ├──────────────┤
|
||||
│ id (PK) │──────▶│ id (PK) │
|
||||
│ field │ │ foreign_key │
|
||||
└──────────────┘ └──────────────┘
|
||||
|
||||
data_flow: |
|
||||
[Source] ──▶ [Process] ──▶ [Output]
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
[Storage] [Transform] [Display]
|
||||
|
||||
quality_checklist:
|
||||
- All referenced files exist
|
||||
- All code examples are syntactically correct
|
||||
- No broken internal links
|
||||
- Technical details wrapped in <details>
|
||||
- Plain English explanations for all features
|
||||
- Glossary includes all technical terms used
|
||||
- ASCII diagrams render correctly in markdown
|
||||
|
|
@ -0,0 +1,274 @@
|
|||
# Documentation Output Schema
|
||||
# Defines the structure for generated documentation
|
||||
|
||||
version: "1.0"
|
||||
description: Schema for dual-audience project documentation
|
||||
|
||||
output_files:
|
||||
main_documentation:
|
||||
filename: PROJECT_DOCUMENTATION.md
|
||||
required: true
|
||||
sections:
|
||||
- executive_summary
|
||||
- quick_start
|
||||
- architecture_overview
|
||||
- features
|
||||
- for_developers
|
||||
- glossary
|
||||
|
||||
quick_reference:
|
||||
filename: QUICK_REFERENCE.md
|
||||
required: true
|
||||
sections:
|
||||
- commands
|
||||
- key_files
|
||||
- api_endpoints
|
||||
- environment_variables
|
||||
|
||||
api_reference:
|
||||
filename: API_REFERENCE.md
|
||||
required: false
|
||||
condition: has_api_endpoints
|
||||
sections:
|
||||
- authentication
|
||||
- endpoints_by_resource
|
||||
- error_codes
|
||||
- rate_limiting
|
||||
|
||||
components:
|
||||
filename: COMPONENTS.md
|
||||
required: false
|
||||
condition: has_ui_components
|
||||
sections:
|
||||
- component_index
|
||||
- component_details
|
||||
- usage_examples
|
||||
|
||||
section_schemas:
|
||||
executive_summary:
|
||||
description: High-level project overview for all audiences
|
||||
fields:
|
||||
project_name:
|
||||
type: string
|
||||
required: true
|
||||
tagline:
|
||||
type: string
|
||||
required: true
|
||||
max_length: 100
|
||||
description: One-line description in plain English
|
||||
what_it_does:
|
||||
type: string
|
||||
required: true
|
||||
description: 2-3 sentences, no technical jargon
|
||||
who_its_for:
|
||||
type: string
|
||||
required: true
|
||||
description: Target audience in plain English
|
||||
key_capabilities:
|
||||
type: array
|
||||
items:
|
||||
capability: string
|
||||
description: string
|
||||
min_items: 3
|
||||
max_items: 8
|
||||
|
||||
quick_start:
|
||||
description: Getting started guide for new users
|
||||
fields:
|
||||
prerequisites:
|
||||
type: array
|
||||
items:
|
||||
tool: string
|
||||
purpose: string # Plain English explanation
|
||||
install_command: string
|
||||
installation_steps:
|
||||
type: array
|
||||
items:
|
||||
step: integer
|
||||
command: string
|
||||
explanation: string # What this does
|
||||
basic_usage:
|
||||
type: string
|
||||
description: Simple example of how to use
|
||||
|
||||
architecture_overview:
|
||||
description: Visual system architecture
|
||||
fields:
|
||||
system_diagram:
|
||||
type: string
|
||||
format: ascii_art
|
||||
required: true
|
||||
technology_stack:
|
||||
type: array
|
||||
items:
|
||||
layer: string
|
||||
technology: string
|
||||
purpose: string # Plain English
|
||||
directory_structure:
|
||||
type: string
|
||||
format: tree
|
||||
required: true
|
||||
|
||||
features:
|
||||
description: Feature documentation for all audiences
|
||||
fields:
|
||||
features:
|
||||
type: array
|
||||
items:
|
||||
name: string
|
||||
what_it_does: string # Plain English
|
||||
how_to_use: string # Simple instructions
|
||||
example: string # Code or usage example
|
||||
technical_notes: string # For engineers, optional
|
||||
|
||||
api_endpoint:
|
||||
description: Single API endpoint documentation
|
||||
fields:
|
||||
method:
|
||||
type: enum
|
||||
values: [GET, POST, PUT, PATCH, DELETE]
|
||||
path:
|
||||
type: string
|
||||
pattern: "^/api/"
|
||||
summary:
|
||||
type: string
|
||||
description: Plain English description
|
||||
description:
|
||||
type: string
|
||||
description: Detailed explanation
|
||||
authentication:
|
||||
type: object
|
||||
fields:
|
||||
required: boolean
|
||||
type: string # bearer, api_key, session
|
||||
request:
|
||||
type: object
|
||||
fields:
|
||||
content_type: string
|
||||
body_schema: object
|
||||
query_params: array
|
||||
path_params: array
|
||||
responses:
|
||||
type: array
|
||||
items:
|
||||
status: integer
|
||||
description: string
|
||||
schema: object
|
||||
examples:
|
||||
type: array
|
||||
items:
|
||||
name: string
|
||||
request: object
|
||||
response: object
|
||||
|
||||
component:
|
||||
description: UI component documentation
|
||||
fields:
|
||||
name:
|
||||
type: string
|
||||
pattern: "^[A-Z][a-zA-Z]*$" # PascalCase
|
||||
path:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
description: Plain English purpose
|
||||
props:
|
||||
type: array
|
||||
items:
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
default: any
|
||||
description: string
|
||||
events:
|
||||
type: array
|
||||
items:
|
||||
name: string
|
||||
payload: string
|
||||
description: string
|
||||
usage_example:
|
||||
type: string
|
||||
format: code
|
||||
dependencies:
|
||||
type: array
|
||||
items: string
|
||||
|
||||
data_model:
|
||||
description: Data model documentation
|
||||
fields:
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
description: What data it represents (plain English)
|
||||
table_name:
|
||||
type: string
|
||||
fields:
|
||||
type: array
|
||||
items:
|
||||
name: string
|
||||
type: string
|
||||
description: string # Plain English
|
||||
constraints: array
|
||||
relations:
|
||||
type: array
|
||||
items:
|
||||
type: enum
|
||||
values: [has_one, has_many, belongs_to, many_to_many]
|
||||
target: string
|
||||
description: string
|
||||
|
||||
glossary_term:
|
||||
description: Technical term definition
|
||||
fields:
|
||||
term:
|
||||
type: string
|
||||
required: true
|
||||
definition:
|
||||
type: string
|
||||
required: true
|
||||
description: Plain English definition
|
||||
see_also:
|
||||
type: array
|
||||
items: string
|
||||
description: Related terms
|
||||
|
||||
audience_markers:
|
||||
non_technical:
|
||||
indicator: "📖"
|
||||
description: "For all readers"
|
||||
technical:
|
||||
indicator: "🔧"
|
||||
description: "For developers"
|
||||
wrapper: "<details><summary>🔧 Technical Details</summary>...content...</details>"
|
||||
|
||||
formatting_rules:
|
||||
headings:
|
||||
h1: "# Title"
|
||||
h2: "## Section"
|
||||
h3: "### Subsection"
|
||||
code_blocks:
|
||||
language_hints: required
|
||||
max_lines: 30
|
||||
tables:
|
||||
alignment: left
|
||||
max_columns: 5
|
||||
diagrams:
|
||||
format: ascii_art
|
||||
max_width: 80
|
||||
links:
|
||||
internal: "[text](#anchor)"
|
||||
external: "[text](url)"
|
||||
file_reference: "`path/to/file`"
|
||||
|
||||
validation_rules:
|
||||
- name: no_broken_links
|
||||
description: All internal links must resolve
|
||||
- name: code_syntax
|
||||
description: All code blocks must be syntactically valid
|
||||
- name: file_references
|
||||
description: All referenced files must exist
|
||||
- name: glossary_coverage
|
||||
description: All technical terms must be in glossary
|
||||
- name: diagram_rendering
|
||||
description: ASCII diagrams must render correctly
|
||||
|
|
@ -0,0 +1,272 @@
|
|||
# Project Analysis Schema
|
||||
# Defines the structure for project analysis output
|
||||
|
||||
version: "1.0"
|
||||
description: Schema for analyzing project structure before documentation generation
|
||||
|
||||
project_analysis:
|
||||
project:
|
||||
type: object
|
||||
required: true
|
||||
fields:
|
||||
name:
|
||||
type: string
|
||||
required: true
|
||||
source: package.json/name or directory name
|
||||
version:
|
||||
type: string
|
||||
required: false
|
||||
source: package.json/version
|
||||
description:
|
||||
type: string
|
||||
required: false
|
||||
source: package.json/description or README.md first paragraph
|
||||
type:
|
||||
type: enum
|
||||
values: [node, python, rust, go, java, dotnet, ruby, php, other]
|
||||
detection:
|
||||
node: package.json
|
||||
python: requirements.txt, pyproject.toml, setup.py
|
||||
rust: Cargo.toml
|
||||
go: go.mod
|
||||
java: pom.xml, build.gradle
|
||||
dotnet: "*.csproj, *.sln"
|
||||
ruby: Gemfile
|
||||
php: composer.json
|
||||
repository:
|
||||
type: string
|
||||
source: package.json/repository or .git/config
|
||||
|
||||
tech_stack:
|
||||
type: object
|
||||
required: true
|
||||
fields:
|
||||
language:
|
||||
type: string
|
||||
description: Primary programming language
|
||||
framework:
|
||||
type: string
|
||||
description: Main application framework
|
||||
detection:
|
||||
next: "next" in dependencies
|
||||
react: "react" in dependencies without "next"
|
||||
vue: "vue" in dependencies
|
||||
angular: "@angular/core" in dependencies
|
||||
express: "express" in dependencies
|
||||
fastapi: "fastapi" in requirements
|
||||
django: "django" in requirements
|
||||
flask: "flask" in requirements
|
||||
rails: "rails" in Gemfile
|
||||
database:
|
||||
type: string
|
||||
description: Database system if any
|
||||
detection:
|
||||
prisma: "@prisma/client" in dependencies
|
||||
mongoose: "mongoose" in dependencies
|
||||
typeorm: "typeorm" in dependencies
|
||||
sequelize: "sequelize" in dependencies
|
||||
sqlalchemy: "sqlalchemy" in requirements
|
||||
ui_framework:
|
||||
type: string
|
||||
description: UI component framework if any
|
||||
detection:
|
||||
tailwind: "tailwindcss" in devDependencies
|
||||
mui: "@mui/material" in dependencies
|
||||
chakra: "@chakra-ui/react" in dependencies
|
||||
shadcn: "shadcn" patterns in components
|
||||
key_dependencies:
|
||||
type: array
|
||||
items:
|
||||
name: string
|
||||
version: string
|
||||
purpose: string # Plain English explanation
|
||||
categorization:
|
||||
core: Framework, runtime dependencies
|
||||
database: ORM, database clients
|
||||
auth: Authentication libraries
|
||||
ui: UI component libraries
|
||||
testing: Test frameworks
|
||||
build: Build tools, bundlers
|
||||
utility: Helper libraries
|
||||
|
||||
structure:
|
||||
type: object
|
||||
required: true
|
||||
fields:
|
||||
source_dir:
|
||||
type: string
|
||||
description: Main source code directory
|
||||
detection:
|
||||
- src/
|
||||
- app/
|
||||
- lib/
|
||||
- source/
|
||||
directories:
|
||||
type: array
|
||||
items:
|
||||
path: string
|
||||
purpose: string # Plain English description
|
||||
file_count: integer
|
||||
key_files: array
|
||||
common_mappings:
|
||||
src/components: "UI components"
|
||||
src/pages: "Application pages/routes"
|
||||
src/api: "API route handlers"
|
||||
src/lib: "Utility functions and shared code"
|
||||
src/hooks: "Custom React hooks"
|
||||
src/context: "React context providers"
|
||||
src/store: "State management"
|
||||
src/types: "TypeScript type definitions"
|
||||
src/styles: "Global styles and themes"
|
||||
prisma/: "Database schema and migrations"
|
||||
public/: "Static assets"
|
||||
tests/: "Test files"
|
||||
__tests__/: "Test files (Jest convention)"
|
||||
|
||||
features:
|
||||
type: array
|
||||
description: Main features/capabilities of the project
|
||||
items:
|
||||
name: string
|
||||
description: string # Plain English
|
||||
technical_notes: string # For engineers
|
||||
files: array # Key file paths
|
||||
detection_patterns:
|
||||
authentication:
|
||||
keywords: [auth, login, logout, session, jwt, oauth]
|
||||
files: ["**/auth/**", "**/login/**"]
|
||||
user_management:
|
||||
keywords: [user, profile, account, register, signup]
|
||||
files: ["**/user/**", "**/users/**"]
|
||||
api:
|
||||
keywords: [api, endpoint, route, handler]
|
||||
files: ["**/api/**", "**/routes/**"]
|
||||
database:
|
||||
keywords: [model, entity, schema, migration, prisma]
|
||||
files: ["**/models/**", "**/prisma/**"]
|
||||
file_upload:
|
||||
keywords: [upload, file, storage, s3, blob]
|
||||
files: ["**/upload/**", "**/storage/**"]
|
||||
search:
|
||||
keywords: [search, filter, query]
|
||||
files: ["**/search/**"]
|
||||
notifications:
|
||||
keywords: [notification, email, sms, push]
|
||||
files: ["**/notification/**", "**/email/**"]
|
||||
|
||||
components:
|
||||
type: array
|
||||
description: UI components found in the project
|
||||
items:
|
||||
id: string # component_<name>
|
||||
name: string # PascalCase
|
||||
path: string
|
||||
description: string
|
||||
props: string # Props summary
|
||||
dependencies: array # Imported components
|
||||
detection:
|
||||
react: "export (default )?(function|const) [A-Z]"
|
||||
vue: "<template>.*<script>"
|
||||
angular: "@Component"
|
||||
|
||||
api_endpoints:
|
||||
type: array
|
||||
description: API endpoints found in the project
|
||||
items:
|
||||
method: enum [GET, POST, PUT, PATCH, DELETE]
|
||||
path: string
|
||||
handler_file: string
|
||||
description: string
|
||||
technical_notes: string
|
||||
detection:
|
||||
next_app_router: "app/api/**/route.ts exports GET, POST, etc."
|
||||
next_pages_router: "pages/api/**/*.ts"
|
||||
express: "router.get/post/put/delete"
|
||||
fastapi: "@app.get/post/put/delete"
|
||||
|
||||
data_models:
|
||||
type: array
|
||||
description: Data models/entities found
|
||||
items:
|
||||
name: string
|
||||
description: string # Plain English
|
||||
fields: array
|
||||
relations: array
|
||||
detection:
|
||||
prisma: "model [A-Z][a-z]+ {"
|
||||
typeorm: "@Entity"
|
||||
mongoose: "new Schema"
|
||||
sqlalchemy: "class.*Base"
|
||||
|
||||
glossary_terms:
|
||||
type: array
|
||||
description: Technical terms that need definitions
|
||||
items:
|
||||
term: string
|
||||
definition: string # Plain English
|
||||
auto_detection:
|
||||
- Acronyms (all caps words)
|
||||
- Framework-specific terms
|
||||
- Domain-specific terminology
|
||||
- Technical jargon from code comments
|
||||
|
||||
analysis_process:
|
||||
steps:
|
||||
1_identify_type:
|
||||
description: Determine project type from config files
|
||||
files_to_check:
|
||||
- package.json
|
||||
- requirements.txt
|
||||
- pyproject.toml
|
||||
- Cargo.toml
|
||||
- go.mod
|
||||
- pom.xml
|
||||
|
||||
2_scan_structure:
|
||||
description: Map directory structure
|
||||
actions:
|
||||
- List all directories
|
||||
- Count files per directory
|
||||
- Identify purpose from names
|
||||
|
||||
3_extract_metadata:
|
||||
description: Get project metadata
|
||||
sources:
|
||||
- package.json (name, version, description, dependencies)
|
||||
- README.md (description, usage)
|
||||
- project_manifest.json (if exists)
|
||||
|
||||
4_identify_features:
|
||||
description: Detect main features
|
||||
methods:
|
||||
- Keyword scanning in file names
|
||||
- Pattern matching in code
|
||||
- Directory structure analysis
|
||||
|
||||
5_map_components:
|
||||
description: Catalog UI components
|
||||
methods:
|
||||
- Scan component directories
|
||||
- Extract props from TypeScript
|
||||
- Find usage patterns
|
||||
|
||||
6_document_apis:
|
||||
description: Document API endpoints
|
||||
methods:
|
||||
- Scan API routes
|
||||
- Extract request/response schemas
|
||||
- Find authentication requirements
|
||||
|
||||
7_model_data:
|
||||
description: Document data models
|
||||
methods:
|
||||
- Parse Prisma schema
|
||||
- Extract TypeORM entities
|
||||
- Find Mongoose schemas
|
||||
|
||||
8_collect_terms:
|
||||
description: Build glossary
|
||||
methods:
|
||||
- Extract acronyms
|
||||
- Find domain terms
|
||||
- Look for jargon in comments
|
||||
|
|
@ -0,0 +1,489 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Project Analyzer for Documentation Generation
|
||||
Analyzes project structure and outputs YAML for documentation generation.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
# Try to import yaml, but provide fallback
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
yaml = None
|
||||
|
||||
|
||||
def detect_project_type(root_path: Path) -> Dict[str, Any]:
|
||||
"""Detect project type from config files."""
|
||||
indicators = {
|
||||
'node': ['package.json'],
|
||||
'python': ['requirements.txt', 'pyproject.toml', 'setup.py', 'Pipfile'],
|
||||
'rust': ['Cargo.toml'],
|
||||
'go': ['go.mod'],
|
||||
'java': ['pom.xml', 'build.gradle', 'build.gradle.kts'],
|
||||
'dotnet': list(root_path.glob('*.csproj')) + list(root_path.glob('*.sln')),
|
||||
'ruby': ['Gemfile'],
|
||||
'php': ['composer.json'],
|
||||
}
|
||||
|
||||
for lang, files in indicators.items():
|
||||
if isinstance(files, list) and isinstance(files[0], str):
|
||||
for f in files:
|
||||
if (root_path / f).exists():
|
||||
return {'type': lang, 'config_file': f}
|
||||
elif files: # Already Path objects from glob
|
||||
return {'type': lang, 'config_file': str(files[0].name)}
|
||||
|
||||
return {'type': 'other', 'config_file': None}
|
||||
|
||||
|
||||
def parse_package_json(root_path: Path) -> Dict[str, Any]:
|
||||
"""Parse package.json for Node.js projects."""
|
||||
pkg_path = root_path / 'package.json'
|
||||
if not pkg_path.exists():
|
||||
return {}
|
||||
|
||||
with open(pkg_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
deps = data.get('dependencies', {})
|
||||
dev_deps = data.get('devDependencies', {})
|
||||
|
||||
# Detect framework
|
||||
framework = None
|
||||
if 'next' in deps:
|
||||
framework = 'Next.js'
|
||||
elif 'react' in deps:
|
||||
framework = 'React'
|
||||
elif 'vue' in deps:
|
||||
framework = 'Vue.js'
|
||||
elif '@angular/core' in deps:
|
||||
framework = 'Angular'
|
||||
elif 'express' in deps:
|
||||
framework = 'Express'
|
||||
elif 'fastify' in deps:
|
||||
framework = 'Fastify'
|
||||
|
||||
# Detect database
|
||||
database = None
|
||||
if '@prisma/client' in deps:
|
||||
database = 'Prisma (PostgreSQL/MySQL/SQLite)'
|
||||
elif 'mongoose' in deps:
|
||||
database = 'MongoDB (Mongoose)'
|
||||
elif 'typeorm' in deps:
|
||||
database = 'TypeORM'
|
||||
elif 'sequelize' in deps:
|
||||
database = 'Sequelize'
|
||||
|
||||
# Detect UI framework
|
||||
ui_framework = None
|
||||
if 'tailwindcss' in dev_deps or 'tailwindcss' in deps:
|
||||
ui_framework = 'Tailwind CSS'
|
||||
if '@mui/material' in deps:
|
||||
ui_framework = 'Material UI'
|
||||
elif '@chakra-ui/react' in deps:
|
||||
ui_framework = 'Chakra UI'
|
||||
|
||||
# Categorize dependencies
|
||||
key_deps = []
|
||||
dep_categories = {
|
||||
'core': ['react', 'next', 'vue', 'angular', 'express', 'fastify'],
|
||||
'database': ['@prisma/client', 'mongoose', 'typeorm', 'sequelize', 'pg', 'mysql2'],
|
||||
'auth': ['next-auth', 'passport', 'jsonwebtoken', '@auth0/nextjs-auth0'],
|
||||
'ui': ['@mui/material', '@chakra-ui/react', 'antd', '@radix-ui'],
|
||||
'state': ['zustand', 'redux', '@reduxjs/toolkit', 'recoil', 'jotai'],
|
||||
'testing': ['jest', 'vitest', '@testing-library/react', 'cypress'],
|
||||
}
|
||||
|
||||
for dep, version in {**deps, **dev_deps}.items():
|
||||
category = 'utility'
|
||||
for cat, patterns in dep_categories.items():
|
||||
if any(p in dep for p in patterns):
|
||||
category = cat
|
||||
break
|
||||
|
||||
if category != 'utility' or dep in ['axios', 'zod', 'date-fns', 'lodash']:
|
||||
key_deps.append({
|
||||
'name': dep,
|
||||
'version': version.replace('^', '').replace('~', ''),
|
||||
'category': category,
|
||||
'purpose': get_dep_purpose(dep)
|
||||
})
|
||||
|
||||
return {
|
||||
'name': data.get('name', 'Unknown'),
|
||||
'version': data.get('version', '0.0.0'),
|
||||
'description': data.get('description', ''),
|
||||
'framework': framework,
|
||||
'database': database,
|
||||
'ui_framework': ui_framework,
|
||||
'key_dependencies': key_deps[:15], # Limit to 15 most important
|
||||
'scripts': data.get('scripts', {})
|
||||
}
|
||||
|
||||
|
||||
def get_dep_purpose(dep_name: str) -> str:
|
||||
"""Get plain English purpose for common dependencies."""
|
||||
purposes = {
|
||||
'react': 'UI component library',
|
||||
'next': 'Full-stack React framework',
|
||||
'vue': 'Progressive UI framework',
|
||||
'express': 'Web server framework',
|
||||
'fastify': 'High-performance web framework',
|
||||
'@prisma/client': 'Database ORM and query builder',
|
||||
'mongoose': 'MongoDB object modeling',
|
||||
'typeorm': 'TypeScript ORM',
|
||||
'sequelize': 'SQL ORM',
|
||||
'next-auth': 'Authentication for Next.js',
|
||||
'passport': 'Authentication middleware',
|
||||
'jsonwebtoken': 'JWT token handling',
|
||||
'@mui/material': 'Material Design components',
|
||||
'@chakra-ui/react': 'Accessible component library',
|
||||
'tailwindcss': 'Utility-first CSS framework',
|
||||
'zustand': 'State management',
|
||||
'redux': 'Predictable state container',
|
||||
'@reduxjs/toolkit': 'Redux development toolkit',
|
||||
'axios': 'HTTP client',
|
||||
'zod': 'Schema validation',
|
||||
'date-fns': 'Date utility functions',
|
||||
'lodash': 'Utility functions',
|
||||
'jest': 'Testing framework',
|
||||
'vitest': 'Fast unit testing',
|
||||
'@testing-library/react': 'React component testing',
|
||||
'cypress': 'End-to-end testing',
|
||||
}
|
||||
return purposes.get(dep_name, 'Utility library')
|
||||
|
||||
|
||||
def scan_directory_structure(root_path: Path) -> Dict[str, Any]:
|
||||
"""Scan and categorize directory structure."""
|
||||
ignore_dirs = {
|
||||
'node_modules', '.git', '.next', '__pycache__', 'venv',
|
||||
'.venv', 'dist', 'build', '.cache', 'coverage', '.turbo'
|
||||
}
|
||||
|
||||
common_purposes = {
|
||||
'src': 'Main source code directory',
|
||||
'app': 'Application code (Next.js App Router)',
|
||||
'pages': 'Page components (Next.js Pages Router)',
|
||||
'components': 'Reusable UI components',
|
||||
'lib': 'Shared utilities and libraries',
|
||||
'utils': 'Utility functions',
|
||||
'hooks': 'Custom React hooks',
|
||||
'context': 'React context providers',
|
||||
'store': 'State management',
|
||||
'styles': 'CSS and styling',
|
||||
'types': 'TypeScript type definitions',
|
||||
'api': 'API route handlers',
|
||||
'services': 'Business logic services',
|
||||
'models': 'Data models/entities',
|
||||
'prisma': 'Database schema and migrations',
|
||||
'public': 'Static assets',
|
||||
'tests': 'Test files',
|
||||
'__tests__': 'Jest test files',
|
||||
'test': 'Test files',
|
||||
'spec': 'Test specifications',
|
||||
'docs': 'Documentation',
|
||||
'scripts': 'Build and utility scripts',
|
||||
'config': 'Configuration files',
|
||||
}
|
||||
|
||||
directories = []
|
||||
source_dir = None
|
||||
|
||||
# Find main source directory
|
||||
for candidate in ['src', 'app', 'lib', 'source']:
|
||||
if (root_path / candidate).is_dir():
|
||||
source_dir = candidate
|
||||
break
|
||||
|
||||
# Scan directories
|
||||
for item in sorted(root_path.iterdir()):
|
||||
if item.is_dir() and item.name not in ignore_dirs and not item.name.startswith('.'):
|
||||
file_count = sum(1 for _ in item.rglob('*') if _.is_file())
|
||||
key_files = [
|
||||
f.name for f in item.iterdir()
|
||||
if f.is_file() and f.suffix in ['.ts', '.tsx', '.js', '.jsx', '.py', '.rs', '.go']
|
||||
][:5]
|
||||
|
||||
directories.append({
|
||||
'path': item.name,
|
||||
'purpose': common_purposes.get(item.name, 'Project directory'),
|
||||
'file_count': file_count,
|
||||
'key_files': key_files
|
||||
})
|
||||
|
||||
return {
|
||||
'source_dir': source_dir or '.',
|
||||
'directories': directories
|
||||
}
|
||||
|
||||
|
||||
def detect_features(root_path: Path) -> List[Dict[str, Any]]:
|
||||
"""Detect main features from code patterns."""
|
||||
features = []
|
||||
|
||||
feature_patterns = {
|
||||
'authentication': {
|
||||
'keywords': ['auth', 'login', 'logout', 'session', 'jwt', 'oauth'],
|
||||
'description': 'User authentication and session management',
|
||||
'technical_notes': 'Handles user login, logout, and session tokens'
|
||||
},
|
||||
'user_management': {
|
||||
'keywords': ['user', 'profile', 'account', 'register', 'signup'],
|
||||
'description': 'User account creation and profile management',
|
||||
'technical_notes': 'CRUD operations for user data'
|
||||
},
|
||||
'api': {
|
||||
'keywords': ['api', 'endpoint', 'route'],
|
||||
'description': 'REST API endpoints for data operations',
|
||||
'technical_notes': 'HTTP handlers for client-server communication'
|
||||
},
|
||||
'database': {
|
||||
'keywords': ['prisma', 'model', 'entity', 'schema', 'migration'],
|
||||
'description': 'Database storage and data persistence',
|
||||
'technical_notes': 'ORM-based data layer with migrations'
|
||||
},
|
||||
'file_upload': {
|
||||
'keywords': ['upload', 'file', 'storage', 's3', 'blob'],
|
||||
'description': 'File upload and storage functionality',
|
||||
'technical_notes': 'Handles file uploads and cloud storage'
|
||||
},
|
||||
'search': {
|
||||
'keywords': ['search', 'filter', 'query'],
|
||||
'description': 'Search and filtering capabilities',
|
||||
'technical_notes': 'Full-text search or database queries'
|
||||
},
|
||||
}
|
||||
|
||||
# Scan for features
|
||||
all_files = list(root_path.rglob('*.ts')) + list(root_path.rglob('*.tsx')) + \
|
||||
list(root_path.rglob('*.js')) + list(root_path.rglob('*.jsx'))
|
||||
|
||||
file_names = [f.stem.lower() for f in all_files]
|
||||
file_paths = [str(f.relative_to(root_path)).lower() for f in all_files]
|
||||
|
||||
for feature_name, config in feature_patterns.items():
|
||||
found_files = []
|
||||
for keyword in config['keywords']:
|
||||
found_files.extend([
|
||||
str(f.relative_to(root_path)) for f in all_files
|
||||
if keyword in str(f).lower()
|
||||
])
|
||||
|
||||
if found_files:
|
||||
features.append({
|
||||
'name': feature_name.replace('_', ' ').title(),
|
||||
'description': config['description'],
|
||||
'technical_notes': config['technical_notes'],
|
||||
'files': list(set(found_files))[:5]
|
||||
})
|
||||
|
||||
return features
|
||||
|
||||
|
||||
def find_components(root_path: Path) -> List[Dict[str, Any]]:
|
||||
"""Find UI components in the project."""
|
||||
components = []
|
||||
component_dirs = ['components', 'src/components', 'app/components']
|
||||
|
||||
for comp_dir in component_dirs:
|
||||
comp_path = root_path / comp_dir
|
||||
if comp_path.exists():
|
||||
for file in comp_path.rglob('*.tsx'):
|
||||
if file.name.startswith('_') or file.name == 'index.tsx':
|
||||
continue
|
||||
|
||||
name = file.stem
|
||||
if name[0].isupper(): # Component names are PascalCase
|
||||
components.append({
|
||||
'id': f'component_{name.lower()}',
|
||||
'name': name,
|
||||
'path': str(file.relative_to(root_path)),
|
||||
'description': f'{name} component',
|
||||
'props': 'See source file'
|
||||
})
|
||||
|
||||
return components[:20] # Limit to 20 components
|
||||
|
||||
|
||||
def find_api_endpoints(root_path: Path) -> List[Dict[str, Any]]:
|
||||
"""Find API endpoints in the project."""
|
||||
endpoints = []
|
||||
|
||||
# Next.js App Router: app/api/**/route.ts
|
||||
api_dir = root_path / 'app' / 'api'
|
||||
if api_dir.exists():
|
||||
for route_file in api_dir.rglob('route.ts'):
|
||||
path_parts = route_file.parent.relative_to(api_dir).parts
|
||||
api_path = '/api/' + '/'.join(path_parts)
|
||||
|
||||
# Read file to detect methods
|
||||
content = route_file.read_text()
|
||||
methods = []
|
||||
for method in ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']:
|
||||
if f'export async function {method}' in content or f'export function {method}' in content:
|
||||
methods.append(method)
|
||||
|
||||
for method in methods:
|
||||
endpoints.append({
|
||||
'method': method,
|
||||
'path': api_path.replace('[', ':').replace(']', ''),
|
||||
'handler_file': str(route_file.relative_to(root_path)),
|
||||
'description': f'{method} {api_path}',
|
||||
'technical_notes': 'Next.js App Router endpoint'
|
||||
})
|
||||
|
||||
# Next.js Pages Router: pages/api/**/*.ts
|
||||
pages_api = root_path / 'pages' / 'api'
|
||||
if pages_api.exists():
|
||||
for api_file in pages_api.rglob('*.ts'):
|
||||
path_parts = api_file.relative_to(pages_api).with_suffix('').parts
|
||||
api_path = '/api/' + '/'.join(path_parts)
|
||||
|
||||
endpoints.append({
|
||||
'method': 'MULTIPLE',
|
||||
'path': api_path.replace('[', ':').replace(']', ''),
|
||||
'handler_file': str(api_file.relative_to(root_path)),
|
||||
'description': f'API endpoint at {api_path}',
|
||||
'technical_notes': 'Next.js Pages Router endpoint'
|
||||
})
|
||||
|
||||
return endpoints
|
||||
|
||||
|
||||
def find_data_models(root_path: Path) -> List[Dict[str, Any]]:
|
||||
"""Find data models in the project."""
|
||||
models = []
|
||||
|
||||
# Prisma schema
|
||||
prisma_schema = root_path / 'prisma' / 'schema.prisma'
|
||||
if prisma_schema.exists():
|
||||
content = prisma_schema.read_text()
|
||||
model_pattern = re.compile(r'model\s+(\w+)\s*\{([^}]+)\}', re.MULTILINE)
|
||||
|
||||
for match in model_pattern.finditer(content):
|
||||
model_name = match.group(1)
|
||||
model_body = match.group(2)
|
||||
|
||||
# Extract fields
|
||||
fields = []
|
||||
for line in model_body.strip().split('\n'):
|
||||
line = line.strip()
|
||||
if line and not line.startswith('@@') and not line.startswith('//'):
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
fields.append({
|
||||
'name': parts[0],
|
||||
'type': parts[1],
|
||||
'description': f'{parts[0]} field'
|
||||
})
|
||||
|
||||
models.append({
|
||||
'name': model_name,
|
||||
'description': f'{model_name} data model',
|
||||
'fields': fields[:10] # Limit fields
|
||||
})
|
||||
|
||||
return models
|
||||
|
||||
|
||||
def collect_glossary_terms(features: List, components: List, endpoints: List) -> List[Dict[str, str]]:
|
||||
"""Collect technical terms that need definitions."""
|
||||
common_terms = {
|
||||
'API': 'Application Programming Interface - a way for different software to communicate',
|
||||
'REST': 'Representational State Transfer - a standard way to design web APIs',
|
||||
'Component': 'A reusable piece of the user interface',
|
||||
'Endpoint': 'A specific URL that the application responds to',
|
||||
'ORM': 'Object-Relational Mapping - connects code to database tables',
|
||||
'JWT': 'JSON Web Token - a secure way to transmit user identity',
|
||||
'CRUD': 'Create, Read, Update, Delete - basic data operations',
|
||||
'Props': 'Properties passed to a component to customize it',
|
||||
'State': 'Data that can change and affects what users see',
|
||||
'Hook': 'A way to add features to React components',
|
||||
'Migration': 'A controlled change to database structure',
|
||||
'Schema': 'The structure/shape of data',
|
||||
'Route': 'A URL path that maps to specific functionality',
|
||||
'Handler': 'Code that responds to a specific request',
|
||||
}
|
||||
|
||||
return [{'term': k, 'definition': v} for k, v in common_terms.items()]
|
||||
|
||||
|
||||
def generate_analysis(root_path: Path) -> Dict[str, Any]:
|
||||
"""Generate complete project analysis."""
|
||||
project_info = detect_project_type(root_path)
|
||||
pkg_info = parse_package_json(root_path) if project_info['type'] == 'node' else {}
|
||||
structure = scan_directory_structure(root_path)
|
||||
features = detect_features(root_path)
|
||||
components = find_components(root_path)
|
||||
endpoints = find_api_endpoints(root_path)
|
||||
models = find_data_models(root_path)
|
||||
glossary = collect_glossary_terms(features, components, endpoints)
|
||||
|
||||
return {
|
||||
'analysis_timestamp': datetime.now().isoformat(),
|
||||
'project': {
|
||||
'name': pkg_info.get('name', root_path.name),
|
||||
'version': pkg_info.get('version', '0.0.0'),
|
||||
'description': pkg_info.get('description', ''),
|
||||
'type': project_info['type'],
|
||||
},
|
||||
'tech_stack': {
|
||||
'language': 'TypeScript' if project_info['type'] == 'node' else project_info['type'],
|
||||
'framework': pkg_info.get('framework'),
|
||||
'database': pkg_info.get('database'),
|
||||
'ui_framework': pkg_info.get('ui_framework'),
|
||||
'key_dependencies': pkg_info.get('key_dependencies', []),
|
||||
},
|
||||
'structure': structure,
|
||||
'features': features,
|
||||
'components': components,
|
||||
'api_endpoints': endpoints,
|
||||
'data_models': models,
|
||||
'glossary_terms': glossary,
|
||||
}
|
||||
|
||||
|
||||
def output_yaml(data: Dict[str, Any], output_path: Optional[Path] = None):
|
||||
"""Output analysis as YAML."""
|
||||
if yaml:
|
||||
output = yaml.dump(data, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
||||
else:
|
||||
# Fallback to JSON if yaml not available
|
||||
output = json.dumps(data, indent=2)
|
||||
|
||||
if output_path:
|
||||
output_path.write_text(output)
|
||||
print(f"Analysis written to: {output_path}")
|
||||
else:
|
||||
print(output)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
root_path = Path.cwd()
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
root_path = Path(sys.argv[1])
|
||||
|
||||
if not root_path.exists():
|
||||
print(f"Error: Path does not exist: {root_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
output_path = None
|
||||
if len(sys.argv) > 2:
|
||||
output_path = Path(sys.argv[2])
|
||||
|
||||
analysis = generate_analysis(root_path)
|
||||
output_yaml(analysis, output_path)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,491 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
HTML Documentation Generator
|
||||
Generates beautiful HTML documentation from project analysis.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
# Try to import yaml
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
yaml = None
|
||||
|
||||
|
||||
def load_template(template_path: Path) -> str:
|
||||
"""Load the HTML template."""
|
||||
with open(template_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def load_analysis(analysis_path: Path) -> Dict[str, Any]:
|
||||
"""Load project analysis from YAML or JSON."""
|
||||
with open(analysis_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
if yaml and (analysis_path.suffix in ['.yml', '.yaml']):
|
||||
return yaml.safe_load(content)
|
||||
return json.loads(content)
|
||||
|
||||
|
||||
def escape_html(text: str) -> str:
|
||||
"""Escape HTML special characters."""
|
||||
if not text:
|
||||
return ''
|
||||
return (str(text)
|
||||
.replace('&', '&')
|
||||
.replace('<', '<')
|
||||
.replace('>', '>')
|
||||
.replace('"', '"')
|
||||
.replace("'", '''))
|
||||
|
||||
|
||||
def generate_capabilities_html(capabilities: List[Dict]) -> str:
|
||||
"""Generate HTML for capabilities cards."""
|
||||
icons = ['✨', '⚡', '🔐', '📊', '🚀', '💡', '🎯', '🔧']
|
||||
html_parts = []
|
||||
|
||||
for i, cap in enumerate(capabilities[:8]):
|
||||
icon = icons[i % len(icons)]
|
||||
html_parts.append(f'''
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-icon">{icon}</div>
|
||||
<div class="card-title">{escape_html(cap.get('capability', cap.get('name', 'Feature')))}</div>
|
||||
</div>
|
||||
<p>{escape_html(cap.get('description', ''))}</p>
|
||||
</div>''')
|
||||
|
||||
return '\n'.join(html_parts)
|
||||
|
||||
|
||||
def generate_prerequisites_html(prerequisites: List[Dict]) -> str:
|
||||
"""Generate HTML for prerequisites table rows."""
|
||||
html_parts = []
|
||||
|
||||
for prereq in prerequisites:
|
||||
tool = prereq.get('tool', prereq.get('name', ''))
|
||||
purpose = prereq.get('purpose', prereq.get('description', ''))
|
||||
html_parts.append(f'''
|
||||
<tr>
|
||||
<td><code>{escape_html(tool)}</code></td>
|
||||
<td>{escape_html(purpose)}</td>
|
||||
</tr>''')
|
||||
|
||||
return '\n'.join(html_parts) if html_parts else '''
|
||||
<tr>
|
||||
<td><code>Node.js</code></td>
|
||||
<td>JavaScript runtime environment</td>
|
||||
</tr>'''
|
||||
|
||||
|
||||
def generate_tech_stack_html(tech_stack: Dict) -> str:
|
||||
"""Generate HTML for technology stack table rows."""
|
||||
html_parts = []
|
||||
|
||||
stack_items = [
|
||||
('Language', tech_stack.get('language')),
|
||||
('Framework', tech_stack.get('framework')),
|
||||
('Database', tech_stack.get('database')),
|
||||
('UI Framework', tech_stack.get('ui_framework')),
|
||||
]
|
||||
|
||||
purposes = {
|
||||
'TypeScript': 'Type-safe JavaScript for better code quality',
|
||||
'JavaScript': 'Programming language for web applications',
|
||||
'Python': 'General-purpose programming language',
|
||||
'Next.js': 'Full-stack React framework with SSR',
|
||||
'React': 'Component-based UI library',
|
||||
'Vue.js': 'Progressive JavaScript framework',
|
||||
'Express': 'Minimal web server framework',
|
||||
'Prisma': 'Type-safe database ORM',
|
||||
'MongoDB': 'NoSQL document database',
|
||||
'PostgreSQL': 'Relational database',
|
||||
'Tailwind CSS': 'Utility-first CSS framework',
|
||||
'Material UI': 'React component library',
|
||||
}
|
||||
|
||||
for layer, tech in stack_items:
|
||||
if tech:
|
||||
purpose = purposes.get(tech, f'{tech} for {layer.lower()}')
|
||||
html_parts.append(f'''
|
||||
<tr>
|
||||
<td>{escape_html(layer)}</td>
|
||||
<td><span class="badge badge-primary">{escape_html(tech)}</span></td>
|
||||
<td>{escape_html(purpose)}</td>
|
||||
</tr>''')
|
||||
|
||||
# Add key dependencies
|
||||
for dep in tech_stack.get('key_dependencies', [])[:5]:
|
||||
html_parts.append(f'''
|
||||
<tr>
|
||||
<td>Dependency</td>
|
||||
<td><span class="badge badge-info">{escape_html(dep.get('name', ''))}</span></td>
|
||||
<td>{escape_html(dep.get('purpose', ''))}</td>
|
||||
</tr>''')
|
||||
|
||||
return '\n'.join(html_parts)
|
||||
|
||||
|
||||
def generate_directory_structure(structure: Dict) -> str:
|
||||
"""Generate directory structure text."""
|
||||
lines = ['project/']
|
||||
|
||||
for i, dir_info in enumerate(structure.get('directories', [])[:10]):
|
||||
prefix = '└── ' if i == len(structure.get('directories', [])) - 1 else '├── '
|
||||
path = dir_info.get('path', '')
|
||||
purpose = dir_info.get('purpose', '')
|
||||
lines.append(f"{prefix}{path}/ # {purpose}")
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def generate_features_html(features: List[Dict]) -> str:
|
||||
"""Generate HTML for features section."""
|
||||
icons = ['🔐', '👤', '🔌', '💾', '📁', '🔍', '📧', '⚙️']
|
||||
html_parts = []
|
||||
|
||||
for i, feature in enumerate(features[:8]):
|
||||
icon = icons[i % len(icons)]
|
||||
name = feature.get('name', 'Feature')
|
||||
description = feature.get('description', '')
|
||||
technical_notes = feature.get('technical_notes', '')
|
||||
files = feature.get('files', [])
|
||||
|
||||
files_html = '\n'.join([f'<li><code>{escape_html(f)}</code></li>' for f in files[:3]])
|
||||
|
||||
html_parts.append(f'''
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">{icon}</div>
|
||||
<div class="feature-content">
|
||||
<h4>{escape_html(name)}</h4>
|
||||
<p>{escape_html(description)}</p>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
🔧 Technical Details
|
||||
<span class="tech-badge">For Engineers</span>
|
||||
</summary>
|
||||
<div>
|
||||
<p>{escape_html(technical_notes)}</p>
|
||||
<p><strong>Key Files:</strong></p>
|
||||
<ul>
|
||||
{files_html}
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>''')
|
||||
|
||||
return '\n'.join(html_parts) if html_parts else '''
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">✨</div>
|
||||
<div class="feature-content">
|
||||
<h4>Core Functionality</h4>
|
||||
<p>Main features of the application.</p>
|
||||
</div>
|
||||
</div>'''
|
||||
|
||||
|
||||
def generate_api_endpoints_html(endpoints: List[Dict]) -> str:
|
||||
"""Generate HTML for API endpoints."""
|
||||
if not endpoints:
|
||||
return '''
|
||||
<p>No API endpoints detected. This project may use a different API pattern or may not have an API layer.</p>'''
|
||||
|
||||
html_parts = ['<table>', '<thead>', '<tr>', '<th>Method</th>', '<th>Endpoint</th>', '<th>Description</th>', '</tr>', '</thead>', '<tbody>']
|
||||
|
||||
for endpoint in endpoints[:15]:
|
||||
method = endpoint.get('method', 'GET')
|
||||
method_class = f'method-{method.lower()}'
|
||||
path = endpoint.get('path', '')
|
||||
description = endpoint.get('description', '')
|
||||
|
||||
html_parts.append(f'''
|
||||
<tr>
|
||||
<td><span class="method {method_class}">{escape_html(method)}</span></td>
|
||||
<td><code>{escape_html(path)}</code></td>
|
||||
<td>{escape_html(description)}</td>
|
||||
</tr>''')
|
||||
|
||||
html_parts.extend(['</tbody>', '</table>'])
|
||||
return '\n'.join(html_parts)
|
||||
|
||||
|
||||
def generate_components_html(components: List[Dict]) -> str:
|
||||
"""Generate HTML for component catalog."""
|
||||
if not components:
|
||||
return '''
|
||||
<p>No UI components detected. This project may not have a frontend layer or uses a different component pattern.</p>'''
|
||||
|
||||
html_parts = []
|
||||
|
||||
for comp in components[:10]:
|
||||
name = comp.get('name', 'Component')
|
||||
description = comp.get('description', f'{name} component')
|
||||
path = comp.get('path', '')
|
||||
props = comp.get('props', 'See source file')
|
||||
|
||||
html_parts.append(f'''
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-icon">🧩</div>
|
||||
<div class="card-title">{escape_html(name)}</div>
|
||||
</div>
|
||||
<p>{escape_html(description)}</p>
|
||||
<p><code>{escape_html(path)}</code></p>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
🔧 Props & Usage
|
||||
<span class="tech-badge">Technical</span>
|
||||
</summary>
|
||||
<div>
|
||||
<p><strong>Props:</strong> {escape_html(props)}</p>
|
||||
|
||||
<h4>Usage Example</h4>
|
||||
<pre><code><{escape_html(name)} /></code></pre>
|
||||
</div>
|
||||
</details>
|
||||
</div>''')
|
||||
|
||||
return '\n'.join(html_parts)
|
||||
|
||||
|
||||
def generate_data_models_html(models: List[Dict]) -> str:
|
||||
"""Generate HTML for data models."""
|
||||
if not models:
|
||||
return '''
|
||||
<p>No data models detected. This project may not use a database or uses a different data pattern.</p>'''
|
||||
|
||||
html_parts = []
|
||||
|
||||
for model in models[:10]:
|
||||
name = model.get('name', 'Model')
|
||||
description = model.get('description', f'{name} data model')
|
||||
fields = model.get('fields', [])
|
||||
|
||||
fields_html = ''
|
||||
if fields:
|
||||
fields_html = '<table><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody>'
|
||||
for field in fields[:10]:
|
||||
field_name = field.get('name', '')
|
||||
field_type = field.get('type', 'unknown')
|
||||
field_desc = field.get('description', '')
|
||||
fields_html += f'''
|
||||
<tr>
|
||||
<td><code>{escape_html(field_name)}</code></td>
|
||||
<td><code>{escape_html(field_type)}</code></td>
|
||||
<td>{escape_html(field_desc)}</td>
|
||||
</tr>'''
|
||||
fields_html += '</tbody></table>'
|
||||
|
||||
html_parts.append(f'''
|
||||
<h3>{escape_html(name)}</h3>
|
||||
<p><strong>What it represents:</strong> {escape_html(description)}</p>
|
||||
{fields_html}''')
|
||||
|
||||
return '\n'.join(html_parts)
|
||||
|
||||
|
||||
def generate_er_diagram(models: List[Dict]) -> str:
|
||||
"""Generate ASCII ER diagram."""
|
||||
if not models:
|
||||
return '''┌─────────────────────────────────────┐
|
||||
│ No data models detected │
|
||||
└─────────────────────────────────────┘'''
|
||||
|
||||
lines = []
|
||||
for model in models[:4]:
|
||||
name = model.get('name', 'Model')
|
||||
fields = model.get('fields', [])[:4]
|
||||
|
||||
width = max(len(name) + 4, max([len(f.get('name', '')) + len(f.get('type', '')) + 5 for f in fields] or [20]))
|
||||
|
||||
lines.append('┌' + '─' * width + '┐')
|
||||
lines.append('│' + f' {name} '.center(width) + '│')
|
||||
lines.append('├' + '─' * width + '┤')
|
||||
|
||||
for field in fields:
|
||||
field_str = f" {field.get('name', '')} : {field.get('type', '')}"
|
||||
lines.append('│' + field_str.ljust(width) + '│')
|
||||
|
||||
lines.append('└' + '─' * width + '┘')
|
||||
lines.append('')
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def generate_glossary_html(terms: List[Dict]) -> str:
|
||||
"""Generate HTML for glossary."""
|
||||
html_parts = []
|
||||
|
||||
for term in terms:
|
||||
word = term.get('term', '')
|
||||
definition = term.get('definition', '')
|
||||
|
||||
html_parts.append(f'''
|
||||
<div class="glossary-term">
|
||||
<span class="glossary-word">{escape_html(word)}</span>
|
||||
<span class="glossary-definition">{escape_html(definition)}</span>
|
||||
</div>''')
|
||||
|
||||
return '\n'.join(html_parts)
|
||||
|
||||
|
||||
def generate_system_diagram(tech_stack: Dict, structure: Dict) -> str:
|
||||
"""Generate ASCII system architecture diagram."""
|
||||
framework = tech_stack.get('framework', 'Application')
|
||||
database = tech_stack.get('database', '')
|
||||
ui = tech_stack.get('ui_framework', 'UI')
|
||||
|
||||
diagram = f'''┌─────────────────────────────────────────────────────────────┐
|
||||
│ Application Architecture │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Client │───▶│ API │───▶│ Database │ │
|
||||
│ │ ({ui or 'UI'}) │ │ ({framework or 'Server'}) │ │ ({database or 'Storage'}) │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘'''
|
||||
return diagram
|
||||
|
||||
|
||||
def generate_html(analysis: Dict, template: str) -> str:
|
||||
"""Generate final HTML from analysis and template."""
|
||||
project = analysis.get('project', {})
|
||||
tech_stack = analysis.get('tech_stack', {})
|
||||
structure = analysis.get('structure', {})
|
||||
features = analysis.get('features', [])
|
||||
components = analysis.get('components', [])
|
||||
endpoints = analysis.get('api_endpoints', [])
|
||||
models = analysis.get('data_models', [])
|
||||
glossary = analysis.get('glossary_terms', [])
|
||||
|
||||
# Basic replacements
|
||||
replacements = {
|
||||
'{{PROJECT_NAME}}': escape_html(project.get('name', 'Project')),
|
||||
'{{VERSION}}': escape_html(project.get('version', '1.0.0')),
|
||||
'{{TAGLINE}}': escape_html(project.get('description', 'Project documentation')),
|
||||
'{{DESCRIPTION}}': escape_html(project.get('description', 'This project provides various features and capabilities.')),
|
||||
'{{AUDIENCE}}': 'Developers, stakeholders, and anyone interested in understanding this project.',
|
||||
'{{GENERATED_DATE}}': datetime.now().strftime('%Y-%m-%d'),
|
||||
}
|
||||
|
||||
# Generate complex sections
|
||||
html = template
|
||||
|
||||
# Replace simple placeholders
|
||||
for key, value in replacements.items():
|
||||
html = html.replace(key, value)
|
||||
|
||||
# Replace capabilities section
|
||||
capabilities = [{'capability': cap.get('name'), 'description': cap.get('description')}
|
||||
for cap in features[:4]] if features else [
|
||||
{'capability': 'Core Features', 'description': 'Main application functionality'},
|
||||
{'capability': 'Easy Integration', 'description': 'Simple setup and configuration'}
|
||||
]
|
||||
|
||||
# Find and replace the capabilities placeholder section
|
||||
cap_html = generate_capabilities_html(capabilities)
|
||||
html = re.sub(
|
||||
r'<!-- CAPABILITIES_PLACEHOLDER -->.*?</div>\s*</div>',
|
||||
f'<!-- Generated Capabilities -->\n{cap_html}',
|
||||
html,
|
||||
flags=re.DOTALL
|
||||
)
|
||||
|
||||
# Replace technology stack
|
||||
html = re.sub(
|
||||
r'<!-- TECH_STACK_PLACEHOLDER -->.*?</tr>',
|
||||
generate_tech_stack_html(tech_stack),
|
||||
html,
|
||||
flags=re.DOTALL
|
||||
)
|
||||
|
||||
# Generate and replace diagrams
|
||||
html = html.replace('{{SYSTEM_DIAGRAM}}', generate_system_diagram(tech_stack, structure))
|
||||
html = html.replace('{{DIRECTORY_STRUCTURE}}', generate_directory_structure(structure))
|
||||
html = html.replace('{{ER_DIAGRAM}}', generate_er_diagram(models))
|
||||
|
||||
# Replace features section
|
||||
html = re.sub(
|
||||
r'<!-- FEATURES_PLACEHOLDER -->.*?</div>\s*</div>\s*</div>',
|
||||
f'<!-- Generated Features -->\n{generate_features_html(features)}',
|
||||
html,
|
||||
flags=re.DOTALL
|
||||
)
|
||||
|
||||
# Replace API endpoints
|
||||
html = re.sub(
|
||||
r'<!-- API_ENDPOINTS_PLACEHOLDER -->.*?</details>',
|
||||
f'<h3>API Endpoints</h3>\n{generate_api_endpoints_html(endpoints)}',
|
||||
html,
|
||||
flags=re.DOTALL
|
||||
)
|
||||
|
||||
# Replace components
|
||||
html = re.sub(
|
||||
r'<!-- COMPONENTS_PLACEHOLDER -->.*?</div>\s*</div>',
|
||||
f'<!-- Generated Components -->\n{generate_components_html(components)}',
|
||||
html,
|
||||
flags=re.DOTALL
|
||||
)
|
||||
|
||||
# Replace data models
|
||||
html = re.sub(
|
||||
r'<!-- DATA_MODELS_PLACEHOLDER -->.*?</table>',
|
||||
f'<!-- Generated Data Models -->\n{generate_data_models_html(models)}',
|
||||
html,
|
||||
flags=re.DOTALL
|
||||
)
|
||||
|
||||
# Replace glossary
|
||||
html = re.sub(
|
||||
r'<!-- GLOSSARY_PLACEHOLDER -->.*?</div>',
|
||||
f'<!-- Generated Glossary -->\n{generate_glossary_html(glossary)}\n</div>',
|
||||
html,
|
||||
flags=re.DOTALL
|
||||
)
|
||||
|
||||
# Clean up remaining placeholders
|
||||
html = re.sub(r'\{\{[A-Z_]+\}\}', '', html)
|
||||
|
||||
return html
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: generate_html.py <analysis.yml> <template.html> [output.html]")
|
||||
sys.exit(1)
|
||||
|
||||
analysis_path = Path(sys.argv[1])
|
||||
template_path = Path(sys.argv[2])
|
||||
output_path = Path(sys.argv[3]) if len(sys.argv) > 3 else Path('documentation.html')
|
||||
|
||||
if not analysis_path.exists():
|
||||
print(f"Error: Analysis file not found: {analysis_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not template_path.exists():
|
||||
print(f"Error: Template file not found: {template_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
analysis = load_analysis(analysis_path)
|
||||
template = load_template(template_path)
|
||||
html = generate_html(analysis, template)
|
||||
|
||||
output_path.write_text(html, encoding='utf-8')
|
||||
print(f"HTML documentation generated: {output_path}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
# Documentation Generator Skill
|
||||
# Generates comprehensive dual-audience documentation
|
||||
|
||||
name: documentation-generator
|
||||
version: "1.0.0"
|
||||
description: |
|
||||
Analyzes project structure and generates comprehensive documentation
|
||||
that serves both technical (engineers) and non-technical audiences.
|
||||
|
||||
triggers:
|
||||
commands:
|
||||
- "/eureka:index"
|
||||
- "/eureka:docs"
|
||||
keywords:
|
||||
- "generate documentation"
|
||||
- "create docs"
|
||||
- "document project"
|
||||
- "project documentation"
|
||||
- "index project"
|
||||
|
||||
agents:
|
||||
- doc-writer
|
||||
|
||||
schemas:
|
||||
- documentation_output.yml
|
||||
- project_analysis.yml
|
||||
|
||||
scripts:
|
||||
- analyze_project.py
|
||||
- generate_html.py
|
||||
|
||||
templates:
|
||||
- documentation.html
|
||||
|
||||
capabilities:
|
||||
- Project structure analysis
|
||||
- Dual-audience documentation generation
|
||||
- ASCII diagram creation
|
||||
- API documentation
|
||||
- Component cataloging
|
||||
- Glossary generation
|
||||
|
||||
outputs:
|
||||
primary:
|
||||
- index.html # Beautiful HTML for non-engineers
|
||||
- PROJECT_DOCUMENTATION.md
|
||||
- QUICK_REFERENCE.md
|
||||
optional:
|
||||
- API_REFERENCE.md
|
||||
- COMPONENTS.md
|
||||
- GLOSSARY.md
|
||||
data:
|
||||
- analysis.yml # Project analysis data
|
||||
|
||||
audience_support:
|
||||
non_technical:
|
||||
- Executive Summary
|
||||
- Feature Guide
|
||||
- Glossary
|
||||
- Visual Diagrams
|
||||
technical:
|
||||
- API Reference
|
||||
- Component Catalog
|
||||
- Data Models
|
||||
- Code Examples
|
||||
|
||||
configuration:
|
||||
default_output_dir: docs
|
||||
supported_formats:
|
||||
- markdown
|
||||
- html
|
||||
default_sections:
|
||||
- executive_summary
|
||||
- architecture_overview
|
||||
- getting_started
|
||||
- features
|
||||
- api_reference
|
||||
- component_catalog
|
||||
- data_models
|
||||
- glossary
|
||||
|
||||
dependencies:
|
||||
required:
|
||||
- Read tool (file access)
|
||||
- Write tool (file creation)
|
||||
- Glob tool (file discovery)
|
||||
- Grep tool (pattern search)
|
||||
optional:
|
||||
- Bash tool (script execution)
|
||||
- Task tool (agent delegation)
|
||||
|
||||
quality_gates:
|
||||
- All referenced files must exist
|
||||
- All code examples must be syntactically valid
|
||||
- All internal links must resolve
|
||||
- Technical details wrapped in collapsible sections
|
||||
- Glossary covers all technical terms used
|
||||
|
|
@ -0,0 +1,962 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{PROJECT_NAME}} - Documentation</title>
|
||||
<style>
|
||||
/* ===== CSS Variables ===== */
|
||||
:root {
|
||||
--color-primary: #2563eb;
|
||||
--color-primary-light: #3b82f6;
|
||||
--color-primary-dark: #1d4ed8;
|
||||
--color-secondary: #7c3aed;
|
||||
--color-success: #10b981;
|
||||
--color-warning: #f59e0b;
|
||||
--color-danger: #ef4444;
|
||||
--color-info: #06b6d4;
|
||||
|
||||
--color-bg: #ffffff;
|
||||
--color-bg-alt: #f8fafc;
|
||||
--color-bg-code: #f1f5f9;
|
||||
--color-text: #1e293b;
|
||||
--color-text-light: #64748b;
|
||||
--color-text-muted: #94a3b8;
|
||||
--color-border: #e2e8f0;
|
||||
|
||||
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
|
||||
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
|
||||
|
||||
--radius-sm: 0.375rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
}
|
||||
|
||||
/* Dark mode */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-bg: #0f172a;
|
||||
--color-bg-alt: #1e293b;
|
||||
--color-bg-code: #334155;
|
||||
--color-text: #f1f5f9;
|
||||
--color-text-light: #94a3b8;
|
||||
--color-text-muted: #64748b;
|
||||
--color-border: #334155;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Reset & Base ===== */
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
line-height: 1.7;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ===== Layout ===== */
|
||||
.layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 280px;
|
||||
height: 100vh;
|
||||
background: var(--color-bg-alt);
|
||||
border-right: 1px solid var(--color-border);
|
||||
overflow-y: auto;
|
||||
padding: 2rem 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 0 1.5rem 1.5rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-version {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.nav-section-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--color-text-muted);
|
||||
padding: 0 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: block;
|
||||
padding: 0.5rem;
|
||||
color: var(--color-text-light);
|
||||
text-decoration: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background: var(--color-border);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-link-icon {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* Main content */
|
||||
.main {
|
||||
flex: 1;
|
||||
margin-left: 280px;
|
||||
padding: 3rem 4rem;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
/* ===== Typography ===== */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-top: 0;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.75rem;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ===== Components ===== */
|
||||
|
||||
/* Hero section */
|
||||
.hero {
|
||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-secondary) 100%);
|
||||
color: white;
|
||||
padding: 3rem;
|
||||
border-radius: var(--radius-lg);
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
color: white;
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.hero-tagline {
|
||||
font-size: 1.25rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border-radius: var(--radius-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Grid */
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.grid-3 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.grid-2, .grid-3 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1.5rem 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--color-bg-alt);
|
||||
font-weight: 600;
|
||||
color: var(--color-text-light);
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background: var(--color-bg-alt);
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.875em;
|
||||
background: var(--color-bg-code);
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--color-bg-code);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 1.25rem;
|
||||
overflow-x: auto;
|
||||
margin: 1.5rem 0;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Diagrams */
|
||||
.diagram {
|
||||
background: var(--color-bg-alt);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 1.5rem;
|
||||
overflow-x: auto;
|
||||
margin: 1.5rem 0;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.4;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border-radius: 9999px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.badge-primary {
|
||||
background: rgba(37, 99, 235, 0.1);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background: rgba(6, 182, 212, 0.1);
|
||||
color: var(--color-info);
|
||||
}
|
||||
|
||||
/* Collapsible technical details */
|
||||
details {
|
||||
background: var(--color-bg-alt);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
padding: 1rem 1.25rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
summary:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
summary::marker {
|
||||
content: '';
|
||||
}
|
||||
|
||||
summary::before {
|
||||
content: '▶';
|
||||
font-size: 0.75rem;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
details[open] summary::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
details > div {
|
||||
padding: 0 1.25rem 1.25rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.tech-badge {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.7rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* API Methods */
|
||||
.method {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.method-get { background: #dcfce7; color: #166534; }
|
||||
.method-post { background: #dbeafe; color: #1e40af; }
|
||||
.method-put { background: #fef3c7; color: #92400e; }
|
||||
.method-patch { background: #fce7f3; color: #9d174d; }
|
||||
.method-delete { background: #fee2e2; color: #991b1b; }
|
||||
|
||||
/* Alerts / Callouts */
|
||||
.callout {
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: var(--radius-md);
|
||||
margin: 1.5rem 0;
|
||||
border-left: 4px solid;
|
||||
}
|
||||
|
||||
.callout-info {
|
||||
background: rgba(6, 182, 212, 0.1);
|
||||
border-color: var(--color-info);
|
||||
}
|
||||
|
||||
.callout-tip {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border-color: var(--color-success);
|
||||
}
|
||||
|
||||
.callout-warning {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
border-color: var(--color-warning);
|
||||
}
|
||||
|
||||
.callout-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
ul, ol {
|
||||
margin: 1rem 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Glossary */
|
||||
.glossary-term {
|
||||
display: flex;
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.glossary-term:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.glossary-word {
|
||||
font-weight: 600;
|
||||
min-width: 150px;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.glossary-definition {
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
/* Feature list */
|
||||
.feature-item {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1.25rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: 1rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.feature-item:hover {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-secondary) 100%);
|
||||
color: white;
|
||||
border-radius: var(--radius-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.feature-content h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.feature-content p {
|
||||
margin: 0;
|
||||
color: var(--color-text-light);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1024px) {
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.main {
|
||||
margin-left: 0;
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
.mobile-menu-btn {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
z-index: 101;
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.mobile-menu-btn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.main {
|
||||
margin-left: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
details {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
details > div {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<button class="mobile-menu-btn" onclick="toggleSidebar()">☰</button>
|
||||
|
||||
<div class="layout">
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<a href="#" class="sidebar-logo">
|
||||
📚 {{PROJECT_NAME}}
|
||||
</a>
|
||||
<div class="sidebar-version">Version {{VERSION}}</div>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">
|
||||
<div class="nav-section-title">Overview</div>
|
||||
<a href="#executive-summary" class="nav-link">
|
||||
<span class="nav-link-icon">📋</span> Executive Summary
|
||||
</a>
|
||||
<a href="#quick-start" class="nav-link">
|
||||
<span class="nav-link-icon">🚀</span> Quick Start
|
||||
</a>
|
||||
<a href="#architecture" class="nav-link">
|
||||
<span class="nav-link-icon">🏗️</span> Architecture
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<div class="nav-section-title">Features</div>
|
||||
<a href="#features" class="nav-link">
|
||||
<span class="nav-link-icon">✨</span> Feature Guide
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<div class="nav-section-title">For Developers</div>
|
||||
<a href="#api-reference" class="nav-link">
|
||||
<span class="nav-link-icon">🔌</span> API Reference
|
||||
</a>
|
||||
<a href="#components" class="nav-link">
|
||||
<span class="nav-link-icon">🧩</span> Components
|
||||
</a>
|
||||
<a href="#data-models" class="nav-link">
|
||||
<span class="nav-link-icon">💾</span> Data Models
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<div class="nav-section-title">Reference</div>
|
||||
<a href="#glossary" class="nav-link">
|
||||
<span class="nav-link-icon">📖</span> Glossary
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main">
|
||||
<!-- Hero Section -->
|
||||
<section class="hero">
|
||||
<h1>{{PROJECT_NAME}}</h1>
|
||||
<p class="hero-tagline">{{TAGLINE}}</p>
|
||||
</section>
|
||||
|
||||
<!-- Executive Summary -->
|
||||
<section id="executive-summary">
|
||||
<h2>📋 Executive Summary</h2>
|
||||
|
||||
<h3>What is {{PROJECT_NAME}}?</h3>
|
||||
<p>{{DESCRIPTION}}</p>
|
||||
|
||||
<h3>Who is it for?</h3>
|
||||
<p>{{AUDIENCE}}</p>
|
||||
|
||||
<h3>Key Capabilities</h3>
|
||||
<div class="grid grid-2">
|
||||
<!-- CAPABILITIES_PLACEHOLDER -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-icon">✨</div>
|
||||
<div class="card-title">{{CAPABILITY_1_NAME}}</div>
|
||||
</div>
|
||||
<p>{{CAPABILITY_1_DESCRIPTION}}</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-icon">⚡</div>
|
||||
<div class="card-title">{{CAPABILITY_2_NAME}}</div>
|
||||
</div>
|
||||
<p>{{CAPABILITY_2_DESCRIPTION}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Quick Start -->
|
||||
<section id="quick-start">
|
||||
<h2>🚀 Quick Start</h2>
|
||||
|
||||
<h3>Prerequisites</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tool</th>
|
||||
<th>Purpose</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- PREREQUISITES_PLACEHOLDER -->
|
||||
<tr>
|
||||
<td><code>{{PREREQ_1_TOOL}}</code></td>
|
||||
<td>{{PREREQ_1_PURPOSE}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Installation</h3>
|
||||
<pre><code>{{INSTALLATION_COMMANDS}}</code></pre>
|
||||
|
||||
<h3>Basic Usage</h3>
|
||||
<pre><code>{{BASIC_USAGE}}</code></pre>
|
||||
</section>
|
||||
|
||||
<!-- Architecture -->
|
||||
<section id="architecture">
|
||||
<h2>🏗️ Architecture Overview</h2>
|
||||
|
||||
<h3>System Diagram</h3>
|
||||
<div class="diagram">{{SYSTEM_DIAGRAM}}</div>
|
||||
|
||||
<h3>Technology Stack</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Layer</th>
|
||||
<th>Technology</th>
|
||||
<th>Purpose</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- TECH_STACK_PLACEHOLDER -->
|
||||
<tr>
|
||||
<td>{{TECH_LAYER}}</td>
|
||||
<td><span class="badge badge-primary">{{TECH_NAME}}</span></td>
|
||||
<td>{{TECH_PURPOSE}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Directory Structure</h3>
|
||||
<pre><code>{{DIRECTORY_STRUCTURE}}</code></pre>
|
||||
</section>
|
||||
|
||||
<!-- Features -->
|
||||
<section id="features">
|
||||
<h2>✨ Features</h2>
|
||||
|
||||
<!-- FEATURES_PLACEHOLDER -->
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">🔐</div>
|
||||
<div class="feature-content">
|
||||
<h4>{{FEATURE_NAME}}</h4>
|
||||
<p>{{FEATURE_DESCRIPTION}}</p>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
🔧 Technical Details
|
||||
<span class="tech-badge">For Engineers</span>
|
||||
</summary>
|
||||
<div>
|
||||
<p>{{FEATURE_TECHNICAL_NOTES}}</p>
|
||||
<p><strong>Key Files:</strong></p>
|
||||
<ul>
|
||||
<li><code>{{FEATURE_FILE_1}}</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- API Reference -->
|
||||
<section id="api-reference">
|
||||
<h2>🔌 API Reference</h2>
|
||||
|
||||
<div class="callout callout-info">
|
||||
<div class="callout-title">ℹ️ About the API</div>
|
||||
<p>This section is primarily for developers who need to integrate with or extend the application.</p>
|
||||
</div>
|
||||
|
||||
<!-- API_ENDPOINTS_PLACEHOLDER -->
|
||||
<h3>{{API_GROUP_NAME}}</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Method</th>
|
||||
<th>Endpoint</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><code>{{API_PATH}}</code></td>
|
||||
<td>{{API_DESCRIPTION}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
📖 Request & Response Details
|
||||
<span class="tech-badge">Technical</span>
|
||||
</summary>
|
||||
<div>
|
||||
<h4>Request</h4>
|
||||
<pre><code>{{API_REQUEST_EXAMPLE}}</code></pre>
|
||||
|
||||
<h4>Response</h4>
|
||||
<pre><code>{{API_RESPONSE_EXAMPLE}}</code></pre>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<!-- Components -->
|
||||
<section id="components">
|
||||
<h2>🧩 Component Catalog</h2>
|
||||
|
||||
<!-- COMPONENTS_PLACEHOLDER -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-icon">🧩</div>
|
||||
<div class="card-title">{{COMPONENT_NAME}}</div>
|
||||
</div>
|
||||
<p>{{COMPONENT_DESCRIPTION}}</p>
|
||||
<p><code>{{COMPONENT_PATH}}</code></p>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
🔧 Props & Usage
|
||||
<span class="tech-badge">Technical</span>
|
||||
</summary>
|
||||
<div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Prop</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>{{PROP_NAME}}</code></td>
|
||||
<td><code>{{PROP_TYPE}}</code></td>
|
||||
<td>{{PROP_REQUIRED}}</td>
|
||||
<td>{{PROP_DESCRIPTION}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>Usage Example</h4>
|
||||
<pre><code>{{COMPONENT_USAGE_EXAMPLE}}</code></pre>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Data Models -->
|
||||
<section id="data-models">
|
||||
<h2>💾 Data Models</h2>
|
||||
|
||||
<h3>Entity Relationship Diagram</h3>
|
||||
<div class="diagram">{{ER_DIAGRAM}}</div>
|
||||
|
||||
<!-- DATA_MODELS_PLACEHOLDER -->
|
||||
<h3>{{MODEL_NAME}}</h3>
|
||||
<p><strong>What it represents:</strong> {{MODEL_DESCRIPTION}}</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>{{FIELD_NAME}}</code></td>
|
||||
<td><code>{{FIELD_TYPE}}</code></td>
|
||||
<td>{{FIELD_DESCRIPTION}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- Glossary -->
|
||||
<section id="glossary">
|
||||
<h2>📖 Glossary</h2>
|
||||
|
||||
<div class="callout callout-tip">
|
||||
<div class="callout-title">💡 Tip</div>
|
||||
<p>This glossary explains technical terms in plain English. Perfect for non-technical stakeholders!</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<!-- GLOSSARY_PLACEHOLDER -->
|
||||
<div class="glossary-term">
|
||||
<span class="glossary-word">{{TERM}}</span>
|
||||
<span class="glossary-definition">{{DEFINITION}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer style="margin-top: 4rem; padding-top: 2rem; border-top: 1px solid var(--color-border); color: var(--color-text-muted); text-align: center;">
|
||||
<p>Generated by <strong>Eureka Index</strong> · {{GENERATED_DATE}}</p>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Mobile menu toggle
|
||||
function toggleSidebar() {
|
||||
document.getElementById('sidebar').classList.toggle('open');
|
||||
}
|
||||
|
||||
// Active nav highlighting
|
||||
const sections = document.querySelectorAll('section[id]');
|
||||
const navLinks = document.querySelectorAll('.nav-link');
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
let current = '';
|
||||
sections.forEach(section => {
|
||||
const sectionTop = section.offsetTop;
|
||||
if (scrollY >= sectionTop - 100) {
|
||||
current = section.getAttribute('id');
|
||||
}
|
||||
});
|
||||
|
||||
navLinks.forEach(link => {
|
||||
link.classList.remove('active');
|
||||
if (link.getAttribute('href') === '#' + current) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Close sidebar when clicking a link (mobile)
|
||||
navLinks.forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
if (window.innerWidth < 1025) {
|
||||
document.getElementById('sidebar').classList.remove('open');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
# Architect Agent Definition
|
||||
# Responsible for manifest design and task creation
|
||||
|
||||
name: architect
|
||||
role: System Designer & Task Planner
|
||||
|
||||
description: |
|
||||
The Architect designs the system by defining entities in the manifest
|
||||
and breaking down implementation into discrete tasks for other agents.
|
||||
|
||||
allowed_tools:
|
||||
- Read # Read any file for context
|
||||
- Write # Write to manifest and task files ONLY
|
||||
|
||||
blocked_tools:
|
||||
- Bash # Cannot execute commands
|
||||
- Edit # Cannot modify existing code
|
||||
|
||||
allowed_files:
|
||||
- project_manifest.json
|
||||
- "tasks/*.yml"
|
||||
- "tasks/**/*.yml"
|
||||
|
||||
responsibilities:
|
||||
- Design system architecture in manifest
|
||||
- Define entities (pages, components, APIs, tables)
|
||||
- Create implementation tasks for frontend/backend agents
|
||||
- Set task priorities and dependencies
|
||||
- Ensure no orphan entities or circular dependencies
|
||||
|
||||
outputs:
|
||||
- Updated project_manifest.json with new entities
|
||||
- Task files in tasks/ directory
|
||||
|
||||
cannot_do:
|
||||
- Implement any code
|
||||
- Run build/test commands
|
||||
- Modify existing source files
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
# Backend Agent Definition
|
||||
# Responsible for API endpoints and database implementation
|
||||
|
||||
name: backend
|
||||
role: Backend Developer
|
||||
|
||||
description: |
|
||||
The Backend agent implements API endpoints, database schemas,
|
||||
and server-side logic based on approved entities and assigned tasks.
|
||||
|
||||
allowed_tools:
|
||||
- Read # Read files for context
|
||||
- Write # Create new files
|
||||
- Edit # Modify existing files
|
||||
- Bash # Run build, lint, type-check, tests
|
||||
|
||||
blocked_tools: [] # Full access for implementation
|
||||
|
||||
allowed_files:
|
||||
- "app/api/**/*"
|
||||
- "app/lib/**/*"
|
||||
- "prisma/**/*"
|
||||
- "db/**/*"
|
||||
- "*.config.*"
|
||||
|
||||
responsibilities:
|
||||
- Implement API route handlers (GET, POST, PUT, DELETE)
|
||||
- Create database schemas and migrations
|
||||
- Implement data access layer (CRUD operations)
|
||||
- Ensure request/response match manifest specs
|
||||
- Handle errors appropriately
|
||||
- Run lint/type-check before marking complete
|
||||
|
||||
task_types:
|
||||
- create # New API/DB entity
|
||||
- update # Modify existing backend
|
||||
- refactor # Improve code quality
|
||||
- delete # Remove deprecated endpoints
|
||||
|
||||
workflow:
|
||||
1. Read assigned task from tasks/*.yml
|
||||
2. Verify entity is APPROVED in manifest
|
||||
3. Implement code matching manifest spec
|
||||
4. Run validation (lint, type-check)
|
||||
5. Update task status to "review"
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# Frontend Agent Definition
|
||||
# Responsible for UI component and page implementation
|
||||
|
||||
name: frontend
|
||||
role: Frontend Developer
|
||||
|
||||
description: |
|
||||
The Frontend agent implements UI components and pages based on
|
||||
approved entities in the manifest and assigned tasks.
|
||||
|
||||
allowed_tools:
|
||||
- Read # Read files for context
|
||||
- Write # Create new files
|
||||
- Edit # Modify existing files
|
||||
- Bash # Run build, lint, type-check
|
||||
|
||||
blocked_tools: [] # Full access for implementation
|
||||
|
||||
allowed_files:
|
||||
- "app/components/**/*"
|
||||
- "app/**/page.tsx"
|
||||
- "app/**/layout.tsx"
|
||||
- "app/globals.css"
|
||||
- "*.config.*"
|
||||
|
||||
responsibilities:
|
||||
- Implement UI components matching manifest specs
|
||||
- Create pages with correct routing
|
||||
- Ensure props match manifest definitions
|
||||
- Follow existing code patterns and styles
|
||||
- Run lint/type-check before marking complete
|
||||
|
||||
task_types:
|
||||
- create # New component/page
|
||||
- update # Modify existing UI
|
||||
- refactor # Improve code quality
|
||||
- delete # Remove deprecated UI
|
||||
|
||||
workflow:
|
||||
1. Read assigned task from tasks/*.yml
|
||||
2. Verify entity is APPROVED in manifest
|
||||
3. Implement code matching manifest spec
|
||||
4. Run validation (lint, type-check)
|
||||
5. Update task status to "review"
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
# Orchestrator Agent Definition
|
||||
# Coordinates the entire workflow and delegates to specialized agents
|
||||
|
||||
name: orchestrator
|
||||
role: Workflow Coordinator
|
||||
|
||||
description: |
|
||||
The Orchestrator manages the end-to-end workflow, delegating tasks
|
||||
to specialized agents based on task type and current phase.
|
||||
|
||||
workflow_phases:
|
||||
1_design:
|
||||
description: Design system entities in manifest
|
||||
agent: architect
|
||||
inputs: Feature requirements
|
||||
outputs: Updated manifest with DEFINED entities
|
||||
|
||||
2_plan:
|
||||
description: Create implementation tasks
|
||||
agent: architect
|
||||
inputs: Approved manifest entities
|
||||
outputs: Task files in tasks/*.yml
|
||||
|
||||
3_implement:
|
||||
description: Implement tasks by type
|
||||
agents:
|
||||
frontend: UI components, pages
|
||||
backend: API endpoints, database
|
||||
inputs: Tasks with status "pending"
|
||||
outputs: Implemented code, tasks with status "review"
|
||||
|
||||
4_review:
|
||||
description: Review implementations
|
||||
agent: reviewer
|
||||
inputs: Tasks with status "review"
|
||||
outputs: Approved tasks or change requests
|
||||
|
||||
5_complete:
|
||||
description: Mark tasks as done
|
||||
agent: orchestrator
|
||||
inputs: Tasks with status "approved"
|
||||
outputs: Tasks with status "completed"
|
||||
|
||||
delegation_rules:
|
||||
# Task assignment by entity type
|
||||
entity_routing:
|
||||
pages: frontend
|
||||
components: frontend
|
||||
api_endpoints: backend
|
||||
database_tables: backend
|
||||
|
||||
# Task assignment by task type
|
||||
task_routing:
|
||||
create: frontend | backend # Based on entity type
|
||||
update: frontend | backend # Based on entity type
|
||||
delete: frontend | backend # Based on entity type
|
||||
refactor: frontend | backend # Based on entity type
|
||||
review: reviewer
|
||||
test: reviewer
|
||||
|
||||
status_transitions:
|
||||
pending:
|
||||
- in_progress # When agent starts work
|
||||
- blocked # If dependencies not met
|
||||
|
||||
in_progress:
|
||||
- review # When implementation complete
|
||||
- blocked # If blocked by issue
|
||||
|
||||
review:
|
||||
- approved # Reviewer accepts
|
||||
- in_progress # Reviewer requests changes
|
||||
|
||||
approved:
|
||||
- completed # Final state
|
||||
|
||||
blocked:
|
||||
- pending # When blocker resolved
|
||||
|
||||
commands:
|
||||
- /workflow:start <feature> # Start new feature workflow
|
||||
- /workflow:plan # Create tasks from manifest
|
||||
- /workflow:assign # Assign tasks to agents
|
||||
- /workflow:status # Show workflow status
|
||||
- /workflow:next # Process next available task
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
# Reviewer Agent Definition
|
||||
# Responsible for code review and quality assurance
|
||||
|
||||
name: reviewer
|
||||
role: Code Reviewer & QA
|
||||
|
||||
description: |
|
||||
The Reviewer agent reviews implementations, runs tests,
|
||||
and approves or requests changes. Cannot modify code directly.
|
||||
|
||||
allowed_tools:
|
||||
- Read # Read any file for review
|
||||
- Bash # Run tests, lint, type-check, verify
|
||||
|
||||
blocked_tools:
|
||||
- Write # Cannot create files
|
||||
- Edit # Cannot modify files
|
||||
|
||||
allowed_files:
|
||||
- "*" # Can read everything
|
||||
|
||||
responsibilities:
|
||||
- Review implementations match manifest specs
|
||||
- Verify acceptance criteria are met
|
||||
- Run tests and validation commands
|
||||
- Check code quality and patterns
|
||||
- Approve or request changes with feedback
|
||||
|
||||
task_types:
|
||||
- review # Review completed implementation
|
||||
|
||||
review_checklist:
|
||||
- File exists at manifest file_path
|
||||
- Exports match manifest definitions
|
||||
- Props/types match manifest specs
|
||||
- Follows project code patterns
|
||||
- Lint passes
|
||||
- Type-check passes
|
||||
- Tests pass (if applicable)
|
||||
|
||||
workflow:
|
||||
1. Read task with status "review"
|
||||
2. Read implementation files
|
||||
3. Run verification commands
|
||||
4. Compare against manifest specs
|
||||
5. Either:
|
||||
- APPROVE: Update task status to "approved"
|
||||
- REQUEST_CHANGES: Add review_notes, set status to "in_progress"
|
||||
|
||||
outputs:
|
||||
- review_notes in task file
|
||||
- status update (approved | in_progress)
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
# Security Reviewer Agent
|
||||
|
||||
**Role**: Security-focused code review and vulnerability assessment
|
||||
|
||||
**Trigger**: `/workflow:security` command or security review phase
|
||||
|
||||
---
|
||||
|
||||
## Agent Capabilities
|
||||
|
||||
### Primary Functions
|
||||
1. **Static Security Analysis**: Pattern-based vulnerability detection
|
||||
2. **OWASP Top 10 Assessment**: Check for common web vulnerabilities
|
||||
3. **Dependency Audit**: Identify vulnerable packages
|
||||
4. **Configuration Review**: Check security settings and configurations
|
||||
5. **Secret Detection**: Find hardcoded credentials and sensitive data
|
||||
|
||||
### Security Categories Analyzed
|
||||
|
||||
| Category | CWE | OWASP | Severity |
|
||||
|----------|-----|-------|----------|
|
||||
| Hardcoded Secrets | CWE-798 | A07 | CRITICAL |
|
||||
| SQL Injection | CWE-89 | A03 | CRITICAL |
|
||||
| Command Injection | CWE-78 | A03 | CRITICAL |
|
||||
| XSS | CWE-79 | A03 | HIGH |
|
||||
| Path Traversal | CWE-22 | A01 | HIGH |
|
||||
| NoSQL Injection | CWE-943 | A03 | HIGH |
|
||||
| SSRF | CWE-918 | A10 | HIGH |
|
||||
| Prototype Pollution | CWE-1321 | A03 | HIGH |
|
||||
| Insecure Auth | CWE-287 | A07 | HIGH |
|
||||
| CORS Misconfiguration | CWE-942 | A01 | MEDIUM |
|
||||
| Sensitive Data Exposure | CWE-200 | A02 | MEDIUM |
|
||||
| Insecure Dependencies | CWE-1104 | A06 | MEDIUM |
|
||||
| Insecure Randomness | CWE-330 | A02 | LOW |
|
||||
| Debug Code | CWE-489 | A05 | LOW |
|
||||
|
||||
---
|
||||
|
||||
## Agent Constraints
|
||||
|
||||
### READ-ONLY MODE
|
||||
- **CANNOT** modify files
|
||||
- **CANNOT** fix issues directly
|
||||
- **CAN** only read, analyze, and report
|
||||
|
||||
### Output Requirements
|
||||
- Must produce structured security report
|
||||
- Must categorize issues by severity
|
||||
- Must provide remediation guidance
|
||||
- Must reference CWE/OWASP standards
|
||||
|
||||
---
|
||||
|
||||
## Execution Flow
|
||||
|
||||
### Step 1: Run Automated Scanner
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/security_scan.py --project-dir . --json
|
||||
```
|
||||
|
||||
### Step 2: Deep Analysis (Task Agent)
|
||||
For each CRITICAL/HIGH issue, perform deeper analysis:
|
||||
- Trace data flow from source to sink
|
||||
- Identify attack vectors
|
||||
- Assess exploitability
|
||||
- Check for existing mitigations
|
||||
|
||||
### Step 3: Dependency Audit
|
||||
```bash
|
||||
npm audit --json 2>/dev/null || echo "{}"
|
||||
```
|
||||
|
||||
### Step 4: Configuration Review
|
||||
Check security-relevant configurations:
|
||||
- CORS settings
|
||||
- CSP headers
|
||||
- Authentication configuration
|
||||
- Session management
|
||||
- Cookie settings
|
||||
|
||||
### Step 5: Manual Code Review Checklist
|
||||
For implemented features, verify:
|
||||
- [ ] Input validation on all user inputs
|
||||
- [ ] Output encoding for XSS prevention
|
||||
- [ ] Parameterized queries for database access
|
||||
- [ ] Proper error handling (no sensitive data in errors)
|
||||
- [ ] Authentication/authorization checks
|
||||
- [ ] HTTPS enforcement
|
||||
- [ ] Secure cookie flags
|
||||
- [ ] Rate limiting on sensitive endpoints
|
||||
|
||||
### Step 6: Generate Report
|
||||
Output comprehensive security report with:
|
||||
- Executive summary
|
||||
- Issue breakdown by severity
|
||||
- Detailed findings with code locations
|
||||
- Remediation recommendations
|
||||
- Risk assessment
|
||||
|
||||
---
|
||||
|
||||
## Report Format
|
||||
|
||||
```
|
||||
+======================================================================+
|
||||
| SECURITY REVIEW REPORT |
|
||||
+======================================================================+
|
||||
| Project: $PROJECT_NAME |
|
||||
| Scan Date: $DATE |
|
||||
| Agent: security-reviewer |
|
||||
+======================================================================+
|
||||
| EXECUTIVE SUMMARY |
|
||||
+----------------------------------------------------------------------+
|
||||
| Risk Level: CRITICAL / HIGH / MEDIUM / LOW / PASS |
|
||||
| Total Issues: X |
|
||||
| Critical: X (immediate action required) |
|
||||
| High: X (fix before production) |
|
||||
| Medium: X (should fix) |
|
||||
| Low: X (consider fixing) |
|
||||
+======================================================================+
|
||||
| CRITICAL FINDINGS |
|
||||
+----------------------------------------------------------------------+
|
||||
| [1] Hardcoded API Key |
|
||||
| File: src/lib/api.ts:15 |
|
||||
| CWE: CWE-798 |
|
||||
| Code: apiKey = "sk-..." |
|
||||
| Risk: Credentials can be extracted from source |
|
||||
| Fix: Use environment variable: process.env.API_KEY |
|
||||
+----------------------------------------------------------------------+
|
||||
| [2] SQL Injection |
|
||||
| File: app/api/users/route.ts:42 |
|
||||
| CWE: CWE-89 |
|
||||
| Code: query(`SELECT * FROM users WHERE id = ${userId}`) |
|
||||
| Risk: Attacker can manipulate database queries |
|
||||
| Fix: Use parameterized query: query($1, [userId]) |
|
||||
+======================================================================+
|
||||
| HIGH FINDINGS |
|
||||
+----------------------------------------------------------------------+
|
||||
| [3] XSS Vulnerability |
|
||||
| File: app/components/Comment.tsx:28 |
|
||||
| ... |
|
||||
+======================================================================+
|
||||
| DEPENDENCY VULNERABILITIES |
|
||||
+----------------------------------------------------------------------+
|
||||
| lodash@4.17.20 - Prototype Pollution (HIGH) |
|
||||
| axios@0.21.0 - SSRF Risk (MEDIUM) |
|
||||
| Fix: npm audit fix |
|
||||
+======================================================================+
|
||||
| RECOMMENDATIONS |
|
||||
+----------------------------------------------------------------------+
|
||||
| 1. Immediately rotate any exposed credentials |
|
||||
| 2. Fix SQL injection before deploying |
|
||||
| 3. Add input validation layer |
|
||||
| 4. Update vulnerable dependencies |
|
||||
| 5. Add security headers middleware |
|
||||
+======================================================================+
|
||||
| VERDICT: FAIL - X critical issues must be fixed |
|
||||
+======================================================================+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration with Workflow
|
||||
|
||||
### In Review Phase
|
||||
The security agent is automatically invoked during `/workflow:review`:
|
||||
1. Review command runs security_scan.py
|
||||
2. If CRITICAL issues found → blocks approval
|
||||
3. Report included in review output
|
||||
|
||||
### Standalone Security Audit
|
||||
Use `/workflow:security` for dedicated security review:
|
||||
- More thorough analysis
|
||||
- Deep code inspection
|
||||
- Dependency audit
|
||||
- Configuration review
|
||||
|
||||
### Remediation Flow
|
||||
After security issues are identified:
|
||||
1. Issues added to task queue as blockers
|
||||
2. Implementation agents fix issues
|
||||
3. Security agent re-validates fixes
|
||||
4. Approval only after clean scan
|
||||
|
||||
---
|
||||
|
||||
## Tool Usage
|
||||
|
||||
### Primary Tools
|
||||
- `Bash`: Run security_scan.py, npm audit
|
||||
- `Read`: Analyze suspicious code patterns
|
||||
- `Grep`: Search for vulnerability patterns
|
||||
|
||||
### Blocked Tools
|
||||
- `Write`: Cannot create files
|
||||
- `Edit`: Cannot modify files
|
||||
- `Task`: Cannot delegate to other agents
|
||||
|
||||
---
|
||||
|
||||
## Exit Conditions
|
||||
|
||||
### PASS
|
||||
- No CRITICAL or HIGH issues
|
||||
- All dependencies up to date or acknowledged
|
||||
- Security configurations reviewed
|
||||
|
||||
### FAIL
|
||||
- Any CRITICAL issue present
|
||||
- Multiple HIGH issues present
|
||||
- Critical dependencies vulnerable
|
||||
|
||||
### WARNING
|
||||
- Only MEDIUM/LOW issues
|
||||
- Some dependencies outdated
|
||||
- Minor configuration concerns
|
||||
|
|
@ -0,0 +1,347 @@
|
|||
# API Contract Schema
|
||||
# The binding agreement between frontend and backend implementations
|
||||
# Generated during design phase, validated during review phase
|
||||
#
|
||||
# This contract ensures:
|
||||
# 1. Backend implements exactly the endpoints frontend expects
|
||||
# 2. Frontend calls endpoints with correct methods/bodies
|
||||
# 3. Both use the same TypeScript types from shared file
|
||||
|
||||
# ============================================================================
|
||||
# CONTRACT METADATA
|
||||
# ============================================================================
|
||||
api_contract:
|
||||
# Links to workflow
|
||||
workflow_version: string # e.g., v001
|
||||
design_document_revision: integer
|
||||
|
||||
# Timestamps
|
||||
generated_at: timestamp
|
||||
validated_at: timestamp | null
|
||||
|
||||
# Contract status
|
||||
status: draft | active | violated
|
||||
|
||||
# ============================================================================
|
||||
# SHARED TYPES (Source of truth for both agents)
|
||||
# ============================================================================
|
||||
# These types are generated into app/types/api.ts
|
||||
# Both frontend and backend MUST import from this file
|
||||
types:
|
||||
description: "TypeScript interfaces shared between frontend and backend"
|
||||
|
||||
type_schema:
|
||||
# Identity
|
||||
id: string # type_<Name> (e.g., type_User, type_CreateUserRequest)
|
||||
name: string # PascalCase type name (exported interface name)
|
||||
|
||||
# Type definition
|
||||
definition:
|
||||
type: object | array | enum | union
|
||||
|
||||
# For object types
|
||||
properties:
|
||||
- name: string # Property name (camelCase)
|
||||
type: string # TypeScript type (string, number, boolean, other type name)
|
||||
required: boolean
|
||||
description: string
|
||||
validation: string # Optional validation rule
|
||||
|
||||
# For enum types
|
||||
enum_values: [string]
|
||||
|
||||
# For union types
|
||||
union_members: [string] # Array of type names or literal types
|
||||
|
||||
# For array types
|
||||
array_item_type: string # Type of array items
|
||||
|
||||
# Usage tracking
|
||||
used_by:
|
||||
requests: [string] # endpoint_ids that use this as request body
|
||||
responses: [string] # endpoint_ids that use this as response
|
||||
models: [string] # model_ids this type represents
|
||||
|
||||
# Example
|
||||
example_type:
|
||||
id: type_User
|
||||
name: User
|
||||
definition:
|
||||
type: object
|
||||
properties:
|
||||
- name: id
|
||||
type: string
|
||||
required: true
|
||||
description: "Unique user identifier"
|
||||
- name: email
|
||||
type: string
|
||||
required: true
|
||||
description: "User email address"
|
||||
validation: email
|
||||
- name: name
|
||||
type: string
|
||||
required: true
|
||||
description: "Display name"
|
||||
- name: createdAt
|
||||
type: Date
|
||||
required: true
|
||||
description: "Account creation timestamp"
|
||||
used_by:
|
||||
responses: [api_get_user, api_create_user, api_list_users]
|
||||
models: [model_user]
|
||||
|
||||
# ============================================================================
|
||||
# ENDPOINT CONTRACTS (Binding specifications)
|
||||
# ============================================================================
|
||||
endpoints:
|
||||
description: "API endpoint contracts with strict request/response typing"
|
||||
|
||||
endpoint_schema:
|
||||
# Identity
|
||||
id: string # api_<verb>_<resource> from design_document
|
||||
|
||||
# HTTP Specification
|
||||
method: GET | POST | PUT | PATCH | DELETE
|
||||
path: string # Exact path with params (e.g., /api/users/:id)
|
||||
|
||||
# Path parameters (extracted from path)
|
||||
path_params:
|
||||
- name: string
|
||||
type: string # TypeScript type
|
||||
description: string
|
||||
|
||||
# Query parameters (for GET requests)
|
||||
query_params:
|
||||
- name: string
|
||||
type: string
|
||||
required: boolean
|
||||
default: any
|
||||
description: string
|
||||
|
||||
# Request body (for POST/PUT/PATCH)
|
||||
request_body:
|
||||
type_id: string # Reference to types section (e.g., type_CreateUserRequest)
|
||||
content_type: application/json
|
||||
|
||||
# Response specification
|
||||
response:
|
||||
# Success response
|
||||
success:
|
||||
status: integer # 200, 201, 204
|
||||
type_id: string # Reference to types section
|
||||
is_array: boolean # If response is array of type
|
||||
|
||||
# Error responses
|
||||
errors:
|
||||
- status: integer # 400, 401, 403, 404, 500
|
||||
type_id: string # Error response type (usually type_ApiError)
|
||||
description: string
|
||||
|
||||
# Authentication
|
||||
auth:
|
||||
required: boolean
|
||||
roles: [string] # Required roles (empty = any authenticated)
|
||||
|
||||
# Contract version for compatibility
|
||||
version: string # Semantic version of this endpoint spec
|
||||
|
||||
# Example
|
||||
example_endpoint:
|
||||
id: api_create_user
|
||||
method: POST
|
||||
path: /api/users
|
||||
path_params: []
|
||||
query_params: []
|
||||
request_body:
|
||||
type_id: type_CreateUserRequest
|
||||
content_type: application/json
|
||||
response:
|
||||
success:
|
||||
status: 201
|
||||
type_id: type_User
|
||||
is_array: false
|
||||
errors:
|
||||
- status: 400
|
||||
type_id: type_ValidationError
|
||||
description: "Invalid request body"
|
||||
- status: 409
|
||||
type_id: type_ApiError
|
||||
description: "Email already exists"
|
||||
auth:
|
||||
required: false
|
||||
roles: []
|
||||
version: "1.0.0"
|
||||
|
||||
# ============================================================================
|
||||
# FRONTEND USAGE CONTRACTS (What frontend expects to call)
|
||||
# ============================================================================
|
||||
frontend_calls:
|
||||
description: "Expected API calls from frontend components/pages"
|
||||
|
||||
call_schema:
|
||||
# Identity
|
||||
id: string # call_<component>_<action>
|
||||
|
||||
# Source
|
||||
source:
|
||||
entity_id: string # page_xxx or component_xxx
|
||||
file_path: string # Expected file location
|
||||
|
||||
# Target endpoint
|
||||
endpoint_id: string # Reference to endpoints section
|
||||
|
||||
# Call context
|
||||
purpose: string # Why this call is made
|
||||
trigger: string # What triggers this call (onLoad, onClick, onSubmit)
|
||||
|
||||
# Data mapping
|
||||
request_mapping:
|
||||
# How component data maps to request
|
||||
from_props: [string] # Props used in request
|
||||
from_state: [string] # State used in request
|
||||
from_form: [string] # Form fields used in request
|
||||
|
||||
response_handling:
|
||||
# How response is handled
|
||||
success_action: string # What happens on success
|
||||
error_action: string # What happens on error
|
||||
|
||||
# Example
|
||||
example_call:
|
||||
id: call_signup_form_submit
|
||||
source:
|
||||
entity_id: component_signup_form
|
||||
file_path: app/components/SignupForm.tsx
|
||||
endpoint_id: api_create_user
|
||||
purpose: "Submit registration form"
|
||||
trigger: onSubmit
|
||||
request_mapping:
|
||||
from_form: [email, name, password]
|
||||
response_handling:
|
||||
success_action: "Redirect to dashboard"
|
||||
error_action: "Display error message"
|
||||
|
||||
# ============================================================================
|
||||
# BACKEND IMPLEMENTATION CONTRACTS (What backend must provide)
|
||||
# ============================================================================
|
||||
backend_routes:
|
||||
description: "Required backend route implementations"
|
||||
|
||||
route_schema:
|
||||
# Identity
|
||||
id: string # route_<verb>_<path>
|
||||
|
||||
# Target endpoint
|
||||
endpoint_id: string # Reference to endpoints section
|
||||
|
||||
# Implementation location
|
||||
file_path: string # Expected file (e.g., app/api/users/route.ts)
|
||||
export_name: string # Exported function name (GET, POST, etc.)
|
||||
|
||||
# Dependencies
|
||||
uses_models: [string] # model_ids this route uses
|
||||
uses_services: [string] # Service files this route depends on
|
||||
|
||||
# Implementation requirements
|
||||
must_validate:
|
||||
- field: string
|
||||
rule: string
|
||||
must_authenticate: boolean
|
||||
must_authorize: [string] # Role checks required
|
||||
|
||||
# Example
|
||||
example_route:
|
||||
id: route_post_users
|
||||
endpoint_id: api_create_user
|
||||
file_path: app/api/users/route.ts
|
||||
export_name: POST
|
||||
uses_models: [model_user]
|
||||
uses_services: [lib/auth.ts, lib/db.ts]
|
||||
must_validate:
|
||||
- field: email
|
||||
rule: email
|
||||
- field: password
|
||||
rule: min:8
|
||||
must_authenticate: false
|
||||
must_authorize: []
|
||||
|
||||
# ============================================================================
|
||||
# VALIDATION RULES
|
||||
# ============================================================================
|
||||
validation_rules:
|
||||
contracts:
|
||||
- "Every frontend_call must reference existing endpoint_id"
|
||||
- "Every backend_route must reference existing endpoint_id"
|
||||
- "Request body type_id must exist in types section"
|
||||
- "Response type_id must exist in types section"
|
||||
- "Path params in endpoint must match :param patterns in path"
|
||||
|
||||
types:
|
||||
- "Every type must have unique id"
|
||||
- "Type references (nested types) must exist"
|
||||
- "Required properties cannot have default values"
|
||||
|
||||
implementation:
|
||||
- "Frontend must import types from shared types file"
|
||||
- "Backend must import types from shared types file"
|
||||
- "HTTP methods must match contract specification"
|
||||
- "Response shapes must conform to type definitions"
|
||||
|
||||
# ============================================================================
|
||||
# GENERATED FILES
|
||||
# ============================================================================
|
||||
generated_files:
|
||||
shared_types:
|
||||
path: app/types/api.ts
|
||||
description: "TypeScript interfaces for all API types"
|
||||
template: |
|
||||
// AUTO-GENERATED - DO NOT EDIT
|
||||
// Source: .workflow/versions/vXXX/contracts/api_contract.yml
|
||||
// Generated: {timestamp}
|
||||
|
||||
// === Types ===
|
||||
{type_definitions}
|
||||
|
||||
// === API Paths (for type-safe fetch calls) ===
|
||||
export const API_PATHS = {
|
||||
{path_constants}
|
||||
} as const;
|
||||
|
||||
// === API Response Types ===
|
||||
{response_type_helpers}
|
||||
|
||||
api_client:
|
||||
path: app/lib/api-client.ts
|
||||
description: "Type-safe API client (optional)"
|
||||
template: |
|
||||
// AUTO-GENERATED - DO NOT EDIT
|
||||
// Type-safe API client generated from contract
|
||||
|
||||
import type { * } from '@/types/api';
|
||||
|
||||
{api_client_methods}
|
||||
|
||||
# ============================================================================
|
||||
# CONTRACT VIOLATION HANDLING
|
||||
# ============================================================================
|
||||
violations:
|
||||
severity_levels:
|
||||
critical:
|
||||
- "Endpoint exists in frontend but not backend"
|
||||
- "Method mismatch (frontend calls POST, backend has GET)"
|
||||
- "Required field missing in implementation"
|
||||
high:
|
||||
- "Response type mismatch"
|
||||
- "Missing error handling for documented errors"
|
||||
medium:
|
||||
- "Extra undocumented endpoint in backend"
|
||||
- "Type property order differs"
|
||||
low:
|
||||
- "Description mismatch"
|
||||
- "Optional field handling differs"
|
||||
|
||||
on_violation:
|
||||
critical: "Block deployment, require immediate fix"
|
||||
high: "Warn in review, require acknowledgment"
|
||||
medium: "Report in review, fix recommended"
|
||||
low: "Log for tracking"
|
||||
|
|
@ -0,0 +1,340 @@
|
|||
# Dependency Graph Schema
|
||||
# Auto-generated from design_document.yml
|
||||
# Determines execution order for parallel task distribution
|
||||
|
||||
# ============================================================================
|
||||
# GRAPH METADATA
|
||||
# ============================================================================
|
||||
dependency_graph:
|
||||
# Links to design document
|
||||
design_version: string # design_document revision this was generated from
|
||||
workflow_version: string # v001, v002, etc.
|
||||
|
||||
# Generation info
|
||||
generated_at: timestamp
|
||||
generator: string # Script that generated this
|
||||
|
||||
# Statistics
|
||||
stats:
|
||||
total_entities: integer
|
||||
total_layers: integer
|
||||
max_parallelism: integer # Max items that can run in parallel
|
||||
critical_path_length: integer # Longest dependency chain
|
||||
|
||||
# ============================================================================
|
||||
# EXECUTION LAYERS
|
||||
# ============================================================================
|
||||
layers:
|
||||
description: "Ordered layers for parallel execution within each layer"
|
||||
|
||||
layer_schema:
|
||||
layer: integer # 1, 2, 3...
|
||||
name: string # Human-readable name
|
||||
description: string # What this layer contains
|
||||
|
||||
# Items in this layer (can run in parallel)
|
||||
items:
|
||||
- id: string # Entity ID (model_*, api_*, page_*, component_*)
|
||||
type: enum # model | api | page | component
|
||||
name: string # Human-readable name
|
||||
|
||||
# Dependencies (all must be in lower layers)
|
||||
depends_on: [string] # Entity IDs this depends on
|
||||
|
||||
# Task mapping
|
||||
task_id: string # task_* ID for implementation
|
||||
agent: enum # frontend | backend
|
||||
|
||||
# Estimated complexity
|
||||
complexity: enum # low | medium | high
|
||||
|
||||
# Layer constraints
|
||||
requires_layers: [integer] # Layer numbers that must complete first
|
||||
parallel_count: integer # Number of items that can run in parallel
|
||||
|
||||
# Example layers
|
||||
example:
|
||||
- layer: 1
|
||||
name: "Data Layer"
|
||||
description: "Database models - no external dependencies"
|
||||
items:
|
||||
- id: model_user
|
||||
type: model
|
||||
name: User
|
||||
depends_on: []
|
||||
task_id: task_create_model_user
|
||||
agent: backend
|
||||
complexity: medium
|
||||
- id: model_post
|
||||
type: model
|
||||
name: Post
|
||||
depends_on: []
|
||||
task_id: task_create_model_post
|
||||
agent: backend
|
||||
complexity: low
|
||||
requires_layers: []
|
||||
parallel_count: 2
|
||||
|
||||
- layer: 2
|
||||
name: "API Layer"
|
||||
description: "REST endpoints - depend on models"
|
||||
items:
|
||||
- id: api_create_user
|
||||
type: api
|
||||
name: "Create User"
|
||||
depends_on: [model_user]
|
||||
task_id: task_create_api_create_user
|
||||
agent: backend
|
||||
complexity: medium
|
||||
- id: api_list_users
|
||||
type: api
|
||||
name: "List Users"
|
||||
depends_on: [model_user]
|
||||
task_id: task_create_api_list_users
|
||||
agent: backend
|
||||
complexity: low
|
||||
requires_layers: [1]
|
||||
parallel_count: 2
|
||||
|
||||
- layer: 3
|
||||
name: "UI Layer"
|
||||
description: "Pages and components - depend on APIs"
|
||||
items:
|
||||
- id: component_user_card
|
||||
type: component
|
||||
name: UserCard
|
||||
depends_on: []
|
||||
task_id: task_create_component_user_card
|
||||
agent: frontend
|
||||
complexity: low
|
||||
- id: page_users
|
||||
type: page
|
||||
name: "Users Page"
|
||||
depends_on: [api_list_users, component_user_card]
|
||||
task_id: task_create_page_users
|
||||
agent: frontend
|
||||
complexity: medium
|
||||
requires_layers: [2]
|
||||
parallel_count: 2
|
||||
|
||||
# ============================================================================
|
||||
# FULL DEPENDENCY MAP
|
||||
# ============================================================================
|
||||
dependency_map:
|
||||
description: "Complete dependency relationships for visualization"
|
||||
|
||||
entry_schema:
|
||||
entity_id:
|
||||
type: enum # model | api | page | component
|
||||
layer: integer # Which layer this belongs to
|
||||
depends_on: [string] # What this entity needs
|
||||
depended_by: [string] # What entities need this
|
||||
|
||||
# Example
|
||||
example:
|
||||
model_user:
|
||||
type: model
|
||||
layer: 1
|
||||
depends_on: []
|
||||
depended_by: [model_post, api_create_user, api_list_users, api_get_user]
|
||||
|
||||
api_create_user:
|
||||
type: api
|
||||
layer: 2
|
||||
depends_on: [model_user]
|
||||
depended_by: [page_user_create, component_user_form]
|
||||
|
||||
page_users:
|
||||
type: page
|
||||
layer: 3
|
||||
depends_on: [api_list_users, component_user_card]
|
||||
depended_by: []
|
||||
|
||||
# ============================================================================
|
||||
# TASK GENERATION MAP
|
||||
# ============================================================================
|
||||
task_map:
|
||||
description: "Maps entities to implementation tasks with context"
|
||||
|
||||
task_entry_schema:
|
||||
entity_id: string # model_user, api_create_user, etc.
|
||||
task_id: string # task_create_model_user
|
||||
layer: integer # Execution layer
|
||||
agent: enum # frontend | backend
|
||||
|
||||
# Context to pass to subagent (snapshot from design_document)
|
||||
context:
|
||||
# For models
|
||||
model_definition:
|
||||
fields: [object]
|
||||
relations: [object]
|
||||
validations: [object]
|
||||
|
||||
# For APIs
|
||||
api_contract:
|
||||
method: string
|
||||
path: string
|
||||
request_body: object
|
||||
responses: [object]
|
||||
auth: object
|
||||
|
||||
# For pages
|
||||
page_definition:
|
||||
path: string
|
||||
data_needs: [object]
|
||||
components: [string]
|
||||
auth: object
|
||||
|
||||
# For components
|
||||
component_definition:
|
||||
props: [object]
|
||||
events: [object]
|
||||
uses_apis: [string]
|
||||
|
||||
# Shared context
|
||||
related_models: [object] # Models this entity interacts with
|
||||
related_apis: [object] # APIs this entity needs/provides
|
||||
|
||||
# Dependencies as task IDs
|
||||
depends_on_tasks: [string] # Task IDs that must complete first
|
||||
|
||||
# Output definition
|
||||
outputs:
|
||||
files: [string] # Files this task will create
|
||||
provides: [string] # Entity IDs this task provides
|
||||
|
||||
# ============================================================================
|
||||
# EXECUTION PLAN
|
||||
# ============================================================================
|
||||
execution_plan:
|
||||
description: "Concrete execution order for workflow orchestrator"
|
||||
|
||||
phase_schema:
|
||||
phase: integer # 1, 2, 3... (maps to layers)
|
||||
|
||||
# Parallel batch within phase
|
||||
parallel_batch:
|
||||
- task_id: string
|
||||
entity_id: string
|
||||
agent: enum
|
||||
|
||||
# Full context blob for subagent
|
||||
context_file: string # Path to context snapshot file
|
||||
|
||||
# Expected outputs
|
||||
expected_files: [string]
|
||||
|
||||
# Validation to run after completion
|
||||
validation:
|
||||
- type: enum # file_exists | lint | typecheck | test
|
||||
target: string
|
||||
|
||||
# Example
|
||||
example:
|
||||
- phase: 1
|
||||
parallel_batch:
|
||||
- task_id: task_create_model_user
|
||||
entity_id: model_user
|
||||
agent: backend
|
||||
context_file: .workflow/versions/v001/contexts/model_user.yml
|
||||
expected_files: [prisma/schema.prisma, app/models/user.ts]
|
||||
validation:
|
||||
- type: typecheck
|
||||
target: app/models/user.ts
|
||||
|
||||
- task_id: task_create_model_post
|
||||
entity_id: model_post
|
||||
agent: backend
|
||||
context_file: .workflow/versions/v001/contexts/model_post.yml
|
||||
expected_files: [prisma/schema.prisma, app/models/post.ts]
|
||||
validation:
|
||||
- type: typecheck
|
||||
target: app/models/post.ts
|
||||
|
||||
- phase: 2
|
||||
parallel_batch:
|
||||
- task_id: task_create_api_create_user
|
||||
entity_id: api_create_user
|
||||
agent: backend
|
||||
context_file: .workflow/versions/v001/contexts/api_create_user.yml
|
||||
expected_files: [app/api/users/route.ts]
|
||||
validation:
|
||||
- type: lint
|
||||
target: app/api/users/route.ts
|
||||
- type: typecheck
|
||||
target: app/api/users/route.ts
|
||||
|
||||
# ============================================================================
|
||||
# CONTEXT SNAPSHOT SCHEMA
|
||||
# ============================================================================
|
||||
context_snapshot:
|
||||
description: "Schema for per-task context files passed to subagents"
|
||||
|
||||
snapshot_schema:
|
||||
# Metadata
|
||||
task_id: string
|
||||
entity_id: string
|
||||
generated_at: timestamp
|
||||
workflow_version: string
|
||||
|
||||
# The entity being implemented
|
||||
target:
|
||||
type: enum # model | api | page | component
|
||||
definition: object # Full definition from design_document
|
||||
|
||||
# Related entities (for reference)
|
||||
related:
|
||||
models: [object] # Model definitions this task needs to know about
|
||||
apis: [object] # API contracts this task needs to know about
|
||||
components: [object] # Component definitions this task needs
|
||||
|
||||
# Dependency chain
|
||||
dependencies:
|
||||
completed: [string] # Entity IDs already implemented
|
||||
pending: [string] # Entity IDs not yet implemented (shouldn't depend on)
|
||||
|
||||
# File context
|
||||
files:
|
||||
to_create: [string] # Files this task should create
|
||||
to_modify: [string] # Files this task may modify
|
||||
reference: [string] # Files to read for context
|
||||
|
||||
# Acceptance criteria
|
||||
acceptance:
|
||||
- criterion: string # What must be true
|
||||
validation: string # How to verify
|
||||
|
||||
# Implementation hints
|
||||
hints:
|
||||
patterns: [string] # Patterns to follow (from existing codebase)
|
||||
avoid: [string] # Anti-patterns to avoid
|
||||
examples: [string] # Example file paths to reference
|
||||
|
||||
# ============================================================================
|
||||
# GRAPH GENERATION RULES
|
||||
# ============================================================================
|
||||
generation_rules:
|
||||
layer_assignment:
|
||||
- "Models with no relations → Layer 1"
|
||||
- "Models with relations to Layer 1 models → Layer 1 (parallel)"
|
||||
- "APIs depending only on models → Layer 2"
|
||||
- "Components with no API deps → Layer 3 (parallel with pages)"
|
||||
- "Pages and components with API deps → Layer 3+"
|
||||
- "Recursive: if all deps in Layer N, assign to Layer N+1"
|
||||
|
||||
parallelism:
|
||||
- "Items in same layer with no inter-dependencies can run in parallel"
|
||||
- "Max parallelism = min(layer_item_count, configured_max_agents)"
|
||||
- "Group by agent type for efficient batching"
|
||||
|
||||
context_generation:
|
||||
- "Include full definition of target entity"
|
||||
- "Include definitions of all direct dependencies"
|
||||
- "Include one-level of indirect dependencies for context"
|
||||
- "Exclude unrelated entities to minimize context size"
|
||||
|
||||
validation:
|
||||
- "No circular dependencies (would prevent layer assignment)"
|
||||
- "All dependency targets must exist in design_document"
|
||||
- "Each entity must be in exactly one layer"
|
||||
- "Layer numbers must be consecutive starting from 1"
|
||||
|
|
@ -0,0 +1,463 @@
|
|||
# Design Document Schema
|
||||
# The source of truth for system design - all tasks derive from this
|
||||
# Created during DESIGNING phase, approved before IMPLEMENTING
|
||||
|
||||
# ============================================================================
|
||||
# DOCUMENT METADATA
|
||||
# ============================================================================
|
||||
design_document:
|
||||
# Links to workflow
|
||||
workflow_version: string # e.g., v001
|
||||
feature: string # Feature being implemented
|
||||
|
||||
# Timestamps
|
||||
created_at: timestamp
|
||||
updated_at: timestamp
|
||||
approved_at: timestamp | null
|
||||
|
||||
# Design status
|
||||
status: draft | review | approved | rejected
|
||||
|
||||
# Revision tracking
|
||||
revision: integer # Increments on changes
|
||||
revision_notes: string # What changed in this revision
|
||||
|
||||
# ============================================================================
|
||||
# LAYER 1: DATA MODELS (ER Diagram)
|
||||
# ============================================================================
|
||||
data_models:
|
||||
description: "Database entities and their relationships"
|
||||
|
||||
model_schema:
|
||||
# Identity
|
||||
id: string # model_<name> (e.g., model_user, model_post)
|
||||
name: string # PascalCase entity name (e.g., User, Post)
|
||||
description: string # What this model represents
|
||||
|
||||
# Table/Collection info
|
||||
table_name: string # snake_case (e.g., users, posts)
|
||||
|
||||
# Fields
|
||||
fields:
|
||||
- name: string # snake_case field name
|
||||
type: enum # string | integer | boolean | datetime | uuid | json | text | float | decimal | enum
|
||||
constraints: [enum] # primary_key | foreign_key | unique | not_null | indexed | auto_increment | default
|
||||
default: any # Default value if constraint includes 'default'
|
||||
enum_values: [string] # If type is 'enum', list valid values
|
||||
description: string # Field purpose
|
||||
|
||||
# Relations to other models
|
||||
relations:
|
||||
- type: enum # has_one | has_many | belongs_to | many_to_many
|
||||
target: string # Target model_id (e.g., model_post)
|
||||
foreign_key: string # FK field name
|
||||
through: string # Junction table for many_to_many
|
||||
on_delete: enum # cascade | set_null | restrict | no_action
|
||||
|
||||
# Indexes
|
||||
indexes:
|
||||
- fields: [string] # Fields in index
|
||||
unique: boolean # Is unique index
|
||||
name: string # Index name
|
||||
|
||||
# Timestamps (common pattern)
|
||||
timestamps: boolean # Auto-add created_at, updated_at
|
||||
soft_delete: boolean # Add deleted_at for soft deletes
|
||||
|
||||
# Validation rules (business logic)
|
||||
validations:
|
||||
- field: string # Field to validate
|
||||
rule: string # Validation rule (e.g., "email", "min:8", "max:100")
|
||||
message: string # Error message
|
||||
|
||||
# Example
|
||||
example_model:
|
||||
id: model_user
|
||||
name: User
|
||||
description: "Application user account"
|
||||
table_name: users
|
||||
fields:
|
||||
- name: id
|
||||
type: uuid
|
||||
constraints: [primary_key]
|
||||
description: "Unique identifier"
|
||||
- name: email
|
||||
type: string
|
||||
constraints: [unique, not_null, indexed]
|
||||
description: "User email address"
|
||||
- name: name
|
||||
type: string
|
||||
constraints: [not_null]
|
||||
description: "Display name"
|
||||
- name: password_hash
|
||||
type: string
|
||||
constraints: [not_null]
|
||||
description: "Bcrypt hashed password"
|
||||
- name: role
|
||||
type: enum
|
||||
enum_values: [user, admin, moderator]
|
||||
constraints: [not_null, default]
|
||||
default: user
|
||||
description: "User role for authorization"
|
||||
relations:
|
||||
- type: has_many
|
||||
target: model_post
|
||||
foreign_key: user_id
|
||||
on_delete: cascade
|
||||
timestamps: true
|
||||
soft_delete: false
|
||||
validations:
|
||||
- field: email
|
||||
rule: email
|
||||
message: "Invalid email format"
|
||||
- field: password_hash
|
||||
rule: min:60
|
||||
message: "Invalid password hash"
|
||||
|
||||
# ============================================================================
|
||||
# LAYER 2: API ENDPOINTS
|
||||
# ============================================================================
|
||||
api_endpoints:
|
||||
description: "REST API endpoints with request/response contracts"
|
||||
|
||||
endpoint_schema:
|
||||
# Identity
|
||||
id: string # api_<verb>_<resource> (e.g., api_create_user)
|
||||
|
||||
# HTTP
|
||||
method: enum # GET | POST | PUT | PATCH | DELETE
|
||||
path: string # URL path (e.g., /api/users/:id)
|
||||
|
||||
# Description
|
||||
summary: string # Short description
|
||||
description: string # Detailed description
|
||||
|
||||
# Tags for grouping
|
||||
tags: [string] # e.g., [users, authentication]
|
||||
|
||||
# Path parameters
|
||||
path_params:
|
||||
- name: string # Parameter name (e.g., id)
|
||||
type: string # Data type
|
||||
description: string
|
||||
|
||||
# Query parameters (for GET)
|
||||
query_params:
|
||||
- name: string # Parameter name
|
||||
type: string # Data type
|
||||
required: boolean
|
||||
default: any
|
||||
description: string
|
||||
|
||||
# Request body (for POST/PUT/PATCH)
|
||||
request_body:
|
||||
content_type: string # application/json
|
||||
schema:
|
||||
type: object | array
|
||||
properties:
|
||||
- name: string
|
||||
type: string
|
||||
required: boolean
|
||||
validations: [string] # Validation rules
|
||||
description: string
|
||||
example: object # Example request body
|
||||
|
||||
# Response schemas by status code
|
||||
responses:
|
||||
- status: integer # HTTP status code
|
||||
description: string
|
||||
schema:
|
||||
type: object | array
|
||||
properties:
|
||||
- name: string
|
||||
type: string
|
||||
example: object
|
||||
|
||||
# Dependencies
|
||||
depends_on_models: [string] # model_ids this endpoint uses
|
||||
depends_on_apis: [string] # api_ids this endpoint calls (internal)
|
||||
|
||||
# Authentication/Authorization
|
||||
auth:
|
||||
required: boolean
|
||||
roles: [string] # Required roles (empty = any authenticated)
|
||||
|
||||
# Rate limiting
|
||||
rate_limit:
|
||||
requests: integer # Max requests
|
||||
window: string # Time window (e.g., "1m", "1h")
|
||||
|
||||
# Example
|
||||
example_endpoint:
|
||||
id: api_create_user
|
||||
method: POST
|
||||
path: /api/users
|
||||
summary: "Create a new user"
|
||||
description: "Register a new user account with email and password"
|
||||
tags: [users, authentication]
|
||||
request_body:
|
||||
content_type: application/json
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
- name: email
|
||||
type: string
|
||||
required: true
|
||||
validations: [email]
|
||||
description: "User email address"
|
||||
- name: name
|
||||
type: string
|
||||
required: true
|
||||
validations: [min:1, max:100]
|
||||
description: "Display name"
|
||||
- name: password
|
||||
type: string
|
||||
required: true
|
||||
validations: [min:8]
|
||||
description: "Password (will be hashed)"
|
||||
example:
|
||||
email: "user@example.com"
|
||||
name: "John Doe"
|
||||
password: "securepass123"
|
||||
responses:
|
||||
- status: 201
|
||||
description: "User created successfully"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
- name: id
|
||||
type: uuid
|
||||
- name: email
|
||||
type: string
|
||||
- name: name
|
||||
type: string
|
||||
- name: created_at
|
||||
type: datetime
|
||||
example:
|
||||
id: "550e8400-e29b-41d4-a716-446655440000"
|
||||
email: "user@example.com"
|
||||
name: "John Doe"
|
||||
created_at: "2025-01-16T10:00:00Z"
|
||||
- status: 400
|
||||
description: "Validation error"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
- name: error
|
||||
type: string
|
||||
- name: details
|
||||
type: array
|
||||
example:
|
||||
error: "Validation failed"
|
||||
details: ["Email is invalid", "Password too short"]
|
||||
- status: 409
|
||||
description: "Email already exists"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
- name: error
|
||||
type: string
|
||||
example:
|
||||
error: "Email already registered"
|
||||
depends_on_models: [model_user]
|
||||
depends_on_apis: []
|
||||
auth:
|
||||
required: false
|
||||
roles: []
|
||||
|
||||
# ============================================================================
|
||||
# LAYER 3: UI PAGES
|
||||
# ============================================================================
|
||||
pages:
|
||||
description: "Application pages/routes"
|
||||
|
||||
page_schema:
|
||||
# Identity
|
||||
id: string # page_<name> (e.g., page_users, page_user_detail)
|
||||
name: string # Human-readable name
|
||||
|
||||
# Routing
|
||||
path: string # URL path (e.g., /users, /users/[id])
|
||||
layout: string # Layout component to use
|
||||
|
||||
# Data requirements
|
||||
data_needs:
|
||||
- api_id: string # API endpoint to call
|
||||
purpose: string # Why this data is needed
|
||||
on_load: boolean # Fetch on page load
|
||||
|
||||
# Components used
|
||||
components: [string] # component_ids used on this page
|
||||
|
||||
# SEO
|
||||
seo:
|
||||
title: string
|
||||
description: string
|
||||
|
||||
# Auth requirements
|
||||
auth:
|
||||
required: boolean
|
||||
roles: [string]
|
||||
redirect: string # Where to redirect if not authorized
|
||||
|
||||
# State management
|
||||
state:
|
||||
local: [string] # Local state variables
|
||||
global: [string] # Global state dependencies
|
||||
|
||||
# Example
|
||||
example_page:
|
||||
id: page_users
|
||||
name: "Users List"
|
||||
path: /users
|
||||
layout: layout_dashboard
|
||||
data_needs:
|
||||
- api_id: api_list_users
|
||||
purpose: "Display user list"
|
||||
on_load: true
|
||||
components: [component_user_list, component_user_card, component_pagination]
|
||||
seo:
|
||||
title: "Users"
|
||||
description: "View all users"
|
||||
auth:
|
||||
required: true
|
||||
roles: [admin]
|
||||
redirect: /login
|
||||
|
||||
# ============================================================================
|
||||
# LAYER 3: UI COMPONENTS
|
||||
# ============================================================================
|
||||
components:
|
||||
description: "Reusable UI components"
|
||||
|
||||
component_schema:
|
||||
# Identity
|
||||
id: string # component_<name> (e.g., component_user_card)
|
||||
name: string # PascalCase component name
|
||||
|
||||
# Props (input)
|
||||
props:
|
||||
- name: string # Prop name
|
||||
type: string # TypeScript type
|
||||
required: boolean
|
||||
default: any
|
||||
description: string
|
||||
|
||||
# Events (output)
|
||||
events:
|
||||
- name: string # Event name (e.g., onClick, onSubmit)
|
||||
payload: string # Payload type
|
||||
description: string
|
||||
|
||||
# API calls (if component fetches data)
|
||||
uses_apis: [string] # api_ids this component calls directly
|
||||
|
||||
# Child components
|
||||
uses_components: [string] # component_ids used inside this component
|
||||
|
||||
# State
|
||||
internal_state: [string] # Internal state variables
|
||||
|
||||
# Styling
|
||||
variants: [string] # Style variants (e.g., primary, secondary)
|
||||
|
||||
# Example
|
||||
example_component:
|
||||
id: component_user_card
|
||||
name: UserCard
|
||||
props:
|
||||
- name: user
|
||||
type: User
|
||||
required: true
|
||||
description: "User object to display"
|
||||
- name: showActions
|
||||
type: boolean
|
||||
required: false
|
||||
default: true
|
||||
description: "Show edit/delete buttons"
|
||||
events:
|
||||
- name: onEdit
|
||||
payload: "User"
|
||||
description: "Fired when edit button clicked"
|
||||
- name: onDelete
|
||||
payload: "string"
|
||||
description: "Fired when delete confirmed, payload is user ID"
|
||||
uses_apis: []
|
||||
uses_components: [component_avatar, component_button]
|
||||
internal_state: [isDeleting]
|
||||
variants: [default, compact]
|
||||
|
||||
# ============================================================================
|
||||
# DEPENDENCY GRAPH (Auto-generated from above)
|
||||
# ============================================================================
|
||||
dependency_graph:
|
||||
description: "Execution order based on dependencies - auto-generated"
|
||||
|
||||
# Layers for parallel execution
|
||||
layers:
|
||||
- layer: 1
|
||||
name: "Data Models"
|
||||
description: "Database schema - no dependencies"
|
||||
items:
|
||||
- id: string # Entity ID
|
||||
type: model # model | api | page | component
|
||||
dependencies: [] # Empty for layer 1
|
||||
|
||||
- layer: 2
|
||||
name: "API Endpoints"
|
||||
description: "Backend APIs - depend on models"
|
||||
items:
|
||||
- id: string
|
||||
type: api
|
||||
dependencies: [string] # model_ids
|
||||
|
||||
- layer: 3
|
||||
name: "UI Layer"
|
||||
description: "Pages and components - depend on APIs"
|
||||
items:
|
||||
- id: string
|
||||
type: page | component
|
||||
dependencies: [string] # api_ids, component_ids
|
||||
|
||||
# Full dependency map for visualization
|
||||
dependency_map:
|
||||
model_user:
|
||||
depends_on: []
|
||||
depended_by: [api_create_user, api_list_users, api_get_user]
|
||||
api_create_user:
|
||||
depends_on: [model_user]
|
||||
depended_by: [page_user_create, component_user_form]
|
||||
page_users:
|
||||
depends_on: [api_list_users, component_user_list]
|
||||
depended_by: []
|
||||
|
||||
# ============================================================================
|
||||
# DESIGN VALIDATION RULES
|
||||
# ============================================================================
|
||||
validation_rules:
|
||||
models:
|
||||
- "Every model must have a primary_key field"
|
||||
- "Foreign keys must reference existing models"
|
||||
- "Relation targets must exist in data_models"
|
||||
- "Enum types must have enum_values defined"
|
||||
|
||||
apis:
|
||||
- "Every API must have at least one response defined"
|
||||
- "POST/PUT/PATCH must have request_body"
|
||||
- "depends_on_models must reference existing models"
|
||||
- "Path params must match :param patterns in path"
|
||||
|
||||
pages:
|
||||
- "data_needs must reference existing api_ids"
|
||||
- "components must reference existing component_ids"
|
||||
- "auth.redirect must be a valid path"
|
||||
|
||||
components:
|
||||
- "uses_apis must reference existing api_ids"
|
||||
- "uses_components must reference existing component_ids"
|
||||
- "No circular component dependencies"
|
||||
|
||||
graph:
|
||||
- "No circular dependencies in dependency_graph"
|
||||
- "All entities must be assigned to a layer"
|
||||
- "Layer N items can only depend on Layer < N items"
|
||||
|
|
@ -0,0 +1,364 @@
|
|||
# Implementation Task Template Schema
|
||||
# Used by the Architect agent to create implementation tasks
|
||||
# Tasks are generated from design_document.yml with full context
|
||||
|
||||
# ============================================================================
|
||||
# TASK DEFINITION
|
||||
# ============================================================================
|
||||
task:
|
||||
# Required fields
|
||||
id: task_<type>_<entity> # e.g., task_create_model_user, task_create_api_users
|
||||
type: create | update | delete | refactor | test | review
|
||||
title: string # Human-readable title
|
||||
agent: frontend | backend # Which agent implements this
|
||||
entity_id: string # Primary entity ID from design_document
|
||||
entity_ids: [string] # All entity IDs this task covers (for multi-entity tasks)
|
||||
status: pending | in_progress | review | approved | completed | blocked
|
||||
|
||||
# Execution layer (from dependency_graph)
|
||||
layer: integer # Which layer this task belongs to (1, 2, 3...)
|
||||
parallel_group: string # Group ID for parallel execution
|
||||
|
||||
# Optional fields
|
||||
description: string # Detailed implementation notes
|
||||
file_paths: [string] # Files to create/modify
|
||||
dependencies: [string] # Task IDs that must complete first
|
||||
acceptance_criteria: [string] # Checklist for completion
|
||||
priority: low | medium | high # Task priority
|
||||
complexity: low | medium | high # Estimated complexity
|
||||
|
||||
# Tracking (set by system)
|
||||
created_at: datetime
|
||||
assigned_at: datetime
|
||||
completed_at: datetime
|
||||
reviewed_by: string
|
||||
review_notes: string
|
||||
|
||||
# ============================================================================
|
||||
# CONTEXT SECTION (Passed to subagent)
|
||||
# ============================================================================
|
||||
# This is the critical section that provides full context to subagents
|
||||
# Generated from design_document.yml during task creation
|
||||
|
||||
context:
|
||||
# Source reference
|
||||
design_version: string # design_document revision
|
||||
workflow_version: string # Workflow version (v001, etc.)
|
||||
context_snapshot_path: string # Path to full context file
|
||||
|
||||
# TARGET: What this task implements
|
||||
target:
|
||||
entity_id: string # model_user, api_create_user, etc.
|
||||
entity_type: model | api | page | component
|
||||
definition: object # Full definition from design_document
|
||||
|
||||
# For models
|
||||
model:
|
||||
name: string
|
||||
table_name: string
|
||||
fields: [field_definition]
|
||||
relations: [relation_definition]
|
||||
validations: [validation_rule]
|
||||
indexes: [index_definition]
|
||||
|
||||
# For APIs
|
||||
api:
|
||||
method: string
|
||||
path: string
|
||||
summary: string
|
||||
path_params: [param_definition]
|
||||
query_params: [param_definition]
|
||||
request_body: object
|
||||
responses: [response_definition]
|
||||
auth: object
|
||||
|
||||
# For pages
|
||||
page:
|
||||
path: string
|
||||
layout: string
|
||||
data_needs: [data_requirement]
|
||||
components: [string]
|
||||
seo: object
|
||||
auth: object
|
||||
|
||||
# For components
|
||||
component:
|
||||
name: string
|
||||
props: [prop_definition]
|
||||
events: [event_definition]
|
||||
uses_apis: [string]
|
||||
uses_components: [string]
|
||||
variants: [string]
|
||||
|
||||
# DEPENDENCIES: What this task needs
|
||||
dependencies:
|
||||
# Models this task interacts with
|
||||
models:
|
||||
- id: string # model_user
|
||||
definition:
|
||||
name: string
|
||||
fields: [field_definition]
|
||||
relations: [relation_definition]
|
||||
|
||||
# APIs this task needs
|
||||
apis:
|
||||
- id: string # api_get_user
|
||||
definition:
|
||||
method: string
|
||||
path: string
|
||||
request_body: object
|
||||
responses: [object]
|
||||
|
||||
# Components this task uses
|
||||
components:
|
||||
- id: string # component_button
|
||||
definition:
|
||||
props: [prop_definition]
|
||||
events: [event_definition]
|
||||
|
||||
# CONTRACTS: Input/Output specifications
|
||||
contracts:
|
||||
# What this task receives from previous tasks
|
||||
inputs:
|
||||
- from_task: string # task_create_model_user
|
||||
provides: string # model_user
|
||||
type: model | api | component | file
|
||||
|
||||
# What this task provides to later tasks
|
||||
outputs:
|
||||
- entity_id: string # api_create_user
|
||||
type: model | api | component | file
|
||||
consumers: [string] # [page_user_create, component_user_form]
|
||||
|
||||
# FILES: File operations
|
||||
files:
|
||||
# Files to create
|
||||
create: [string]
|
||||
|
||||
# Files to modify
|
||||
modify: [string]
|
||||
|
||||
# Files to read for patterns/context
|
||||
reference:
|
||||
- path: string
|
||||
purpose: string # "Similar component pattern", "API route pattern"
|
||||
|
||||
# VALIDATION: How to verify completion
|
||||
validation:
|
||||
# Required checks
|
||||
checks:
|
||||
- type: file_exists | lint | typecheck | test | build
|
||||
target: string # File or test pattern
|
||||
required: boolean
|
||||
|
||||
# Acceptance criteria (human-readable)
|
||||
criteria:
|
||||
- criterion: string
|
||||
verification: string # How to verify this
|
||||
|
||||
# HINTS: Implementation guidance
|
||||
hints:
|
||||
# Patterns to follow
|
||||
patterns:
|
||||
- pattern: string # "Use existing API route pattern"
|
||||
reference: string # "app/api/health/route.ts"
|
||||
|
||||
# Things to avoid
|
||||
avoid:
|
||||
- issue: string
|
||||
reason: string
|
||||
|
||||
# Code examples
|
||||
examples:
|
||||
- description: string
|
||||
file: string
|
||||
|
||||
# ============================================================================
|
||||
# TASK GENERATION RULES
|
||||
# ============================================================================
|
||||
generation_rules:
|
||||
from_model:
|
||||
task_id: "task_create_model_{model_name}"
|
||||
type: create
|
||||
agent: backend
|
||||
file_paths:
|
||||
- "prisma/schema.prisma" # Add model to schema
|
||||
- "app/models/{model_name}.ts" # TypeScript types
|
||||
acceptance_criteria:
|
||||
- "Model defined in Prisma schema"
|
||||
- "TypeScript types exported"
|
||||
- "Relations properly configured"
|
||||
- "Migrations generated"
|
||||
|
||||
from_api:
|
||||
task_id: "task_create_api_{endpoint_name}"
|
||||
type: create
|
||||
agent: backend
|
||||
file_paths:
|
||||
- "app/api/{path}/route.ts"
|
||||
acceptance_criteria:
|
||||
- "Endpoint responds to {method} requests"
|
||||
- "Request validation implemented"
|
||||
- "Response matches contract"
|
||||
- "Auth requirements enforced"
|
||||
- "Error handling complete"
|
||||
|
||||
from_page:
|
||||
task_id: "task_create_page_{page_name}"
|
||||
type: create
|
||||
agent: frontend
|
||||
file_paths:
|
||||
- "app/{path}/page.tsx"
|
||||
acceptance_criteria:
|
||||
- "Page renders at {path}"
|
||||
- "Data fetching implemented"
|
||||
- "Components integrated"
|
||||
- "Auth protection active"
|
||||
- "SEO metadata set"
|
||||
|
||||
from_component:
|
||||
task_id: "task_create_component_{component_name}"
|
||||
type: create
|
||||
agent: frontend
|
||||
file_paths:
|
||||
- "app/components/{ComponentName}.tsx"
|
||||
acceptance_criteria:
|
||||
- "Component renders correctly"
|
||||
- "Props typed and documented"
|
||||
- "Events emitted properly"
|
||||
- "Variants implemented"
|
||||
- "Accessible (a11y)"
|
||||
|
||||
# ============================================================================
|
||||
# VALID STATUS TRANSITIONS
|
||||
# ============================================================================
|
||||
status_transitions:
|
||||
pending:
|
||||
- in_progress # Start work
|
||||
- blocked # Dependencies not met
|
||||
in_progress:
|
||||
- review # Ready for review
|
||||
- blocked # Hit blocker
|
||||
review:
|
||||
- approved # Review passed
|
||||
- in_progress # Changes requested
|
||||
approved:
|
||||
- completed # Final completion
|
||||
blocked:
|
||||
- pending # Blocker resolved
|
||||
- in_progress # Resume after unblock
|
||||
|
||||
# ============================================================================
|
||||
# EXAMPLE: Complete Task with Context
|
||||
# ============================================================================
|
||||
example_task:
|
||||
id: task_create_api_create_user
|
||||
type: create
|
||||
title: "Create User API Endpoint"
|
||||
agent: backend
|
||||
entity_id: api_create_user
|
||||
entity_ids: [api_create_user]
|
||||
status: pending
|
||||
layer: 2
|
||||
parallel_group: "layer_2_apis"
|
||||
description: "Implement POST /api/users endpoint for user registration"
|
||||
file_paths:
|
||||
- app/api/users/route.ts
|
||||
dependencies:
|
||||
- task_create_model_user
|
||||
acceptance_criteria:
|
||||
- "POST /api/users returns 201 on success"
|
||||
- "Validates email format"
|
||||
- "Returns 409 if email exists"
|
||||
- "Hashes password before storage"
|
||||
- "Returns user object without password"
|
||||
priority: high
|
||||
complexity: medium
|
||||
|
||||
context:
|
||||
design_version: "rev_3"
|
||||
workflow_version: "v001"
|
||||
context_snapshot_path: ".workflow/versions/v001/contexts/api_create_user.yml"
|
||||
|
||||
target:
|
||||
entity_id: api_create_user
|
||||
entity_type: api
|
||||
api:
|
||||
method: POST
|
||||
path: /api/users
|
||||
summary: "Create a new user"
|
||||
request_body:
|
||||
type: object
|
||||
properties:
|
||||
email: { type: string, required: true, validation: email }
|
||||
name: { type: string, required: true, validation: "min:1,max:100" }
|
||||
password: { type: string, required: true, validation: "min:8" }
|
||||
responses:
|
||||
- status: 201
|
||||
schema: { id: uuid, email: string, name: string, created_at: datetime }
|
||||
- status: 400
|
||||
schema: { error: string, details: array }
|
||||
- status: 409
|
||||
schema: { error: string }
|
||||
auth:
|
||||
required: false
|
||||
|
||||
dependencies:
|
||||
models:
|
||||
- id: model_user
|
||||
definition:
|
||||
name: User
|
||||
table_name: users
|
||||
fields:
|
||||
- { name: id, type: uuid, constraints: [primary_key] }
|
||||
- { name: email, type: string, constraints: [unique, not_null] }
|
||||
- { name: name, type: string, constraints: [not_null] }
|
||||
- { name: password_hash, type: string, constraints: [not_null] }
|
||||
- { name: created_at, type: datetime, constraints: [not_null] }
|
||||
|
||||
contracts:
|
||||
inputs:
|
||||
- from_task: task_create_model_user
|
||||
provides: model_user
|
||||
type: model
|
||||
outputs:
|
||||
- entity_id: api_create_user
|
||||
type: api
|
||||
consumers: [page_signup, component_signup_form]
|
||||
|
||||
files:
|
||||
create:
|
||||
- app/api/users/route.ts
|
||||
reference:
|
||||
- path: app/api/health/route.ts
|
||||
purpose: "API route pattern"
|
||||
- path: app/lib/db.ts
|
||||
purpose: "Database connection"
|
||||
- path: app/lib/auth.ts
|
||||
purpose: "Password hashing"
|
||||
|
||||
validation:
|
||||
checks:
|
||||
- { type: typecheck, target: "app/api/users/route.ts", required: true }
|
||||
- { type: lint, target: "app/api/users/route.ts", required: true }
|
||||
- { type: test, target: "app/api/users/*.test.ts", required: false }
|
||||
criteria:
|
||||
- criterion: "Returns 201 with user object on success"
|
||||
verification: "curl -X POST /api/users with valid data"
|
||||
- criterion: "Returns 409 if email exists"
|
||||
verification: "curl -X POST /api/users with duplicate email"
|
||||
|
||||
hints:
|
||||
patterns:
|
||||
- pattern: "Use NextResponse for responses"
|
||||
reference: "app/api/health/route.ts"
|
||||
- pattern: "Use Prisma for database operations"
|
||||
reference: "app/lib/db.ts"
|
||||
avoid:
|
||||
- issue: "Don't store plain text passwords"
|
||||
reason: "Security vulnerability - always hash with bcrypt"
|
||||
- issue: "Don't return password_hash in response"
|
||||
reason: "Sensitive data exposure"
|
||||
examples:
|
||||
- description: "Similar API endpoint"
|
||||
file: "app/api/health/route.ts"
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
# Workflow State Schema
|
||||
# Tracks automated workflow progress with approval gates
|
||||
|
||||
workflow_state:
|
||||
# Unique workflow run ID
|
||||
id: string # workflow_<timestamp>
|
||||
|
||||
# Feature/task being implemented
|
||||
feature: string
|
||||
|
||||
# Current phase in the workflow
|
||||
current_phase:
|
||||
enum:
|
||||
- INITIALIZING # Starting workflow
|
||||
- DESIGNING # Architect creating entities/tasks
|
||||
- AWAITING_DESIGN_APPROVAL # Gate 1: User approval needed
|
||||
- DESIGN_APPROVED # User approved design
|
||||
- DESIGN_REJECTED # User rejected, needs revision
|
||||
- IMPLEMENTING # Frontend/Backend working
|
||||
- REVIEWING # Reviewer checking implementation
|
||||
- AWAITING_IMPL_APPROVAL # Gate 2: User approval needed
|
||||
- IMPL_APPROVED # User approved implementation
|
||||
- IMPL_REJECTED # User rejected, needs fixes
|
||||
- COMPLETING # Marking tasks as done
|
||||
- COMPLETED # Workflow finished
|
||||
- PAUSED # User paused workflow
|
||||
- FAILED # Workflow encountered error
|
||||
|
||||
# Approval gates status
|
||||
gates:
|
||||
design_approval:
|
||||
status: pending | approved | rejected
|
||||
approved_at: timestamp | null
|
||||
approved_by: string | null
|
||||
rejection_reason: string | null
|
||||
revision_count: integer
|
||||
|
||||
implementation_approval:
|
||||
status: pending | approved | rejected
|
||||
approved_at: timestamp | null
|
||||
approved_by: string | null
|
||||
rejection_reason: string | null
|
||||
revision_count: integer
|
||||
|
||||
# Progress tracking
|
||||
progress:
|
||||
entities_designed: integer
|
||||
tasks_created: integer
|
||||
tasks_implemented: integer
|
||||
tasks_reviewed: integer
|
||||
tasks_approved: integer
|
||||
tasks_completed: integer
|
||||
|
||||
# Task tracking
|
||||
tasks:
|
||||
pending: [task_id]
|
||||
in_progress: [task_id]
|
||||
review: [task_id]
|
||||
approved: [task_id]
|
||||
completed: [task_id]
|
||||
blocked: [task_id]
|
||||
|
||||
# Timestamps
|
||||
started_at: timestamp
|
||||
updated_at: timestamp
|
||||
completed_at: timestamp | null
|
||||
|
||||
# Error tracking
|
||||
last_error: string | null
|
||||
|
||||
# Resumability
|
||||
resume_point:
|
||||
phase: string
|
||||
task_id: string | null
|
||||
action: string # What to do when resuming
|
||||
|
||||
# Example workflow state file
|
||||
example:
|
||||
id: workflow_20250116_143022
|
||||
feature: "User authentication with OAuth"
|
||||
current_phase: AWAITING_DESIGN_APPROVAL
|
||||
gates:
|
||||
design_approval:
|
||||
status: pending
|
||||
approved_at: null
|
||||
approved_by: null
|
||||
rejection_reason: null
|
||||
revision_count: 0
|
||||
implementation_approval:
|
||||
status: pending
|
||||
approved_at: null
|
||||
approved_by: null
|
||||
rejection_reason: null
|
||||
revision_count: 0
|
||||
progress:
|
||||
entities_designed: 5
|
||||
tasks_created: 8
|
||||
tasks_implemented: 0
|
||||
tasks_reviewed: 0
|
||||
tasks_approved: 0
|
||||
tasks_completed: 0
|
||||
tasks:
|
||||
pending: [task_create_LoginPage, task_create_AuthAPI]
|
||||
in_progress: []
|
||||
review: []
|
||||
approved: []
|
||||
completed: []
|
||||
blocked: []
|
||||
started_at: "2025-01-16T14:30:22Z"
|
||||
updated_at: "2025-01-16T14:35:00Z"
|
||||
completed_at: null
|
||||
last_error: null
|
||||
resume_point:
|
||||
phase: AWAITING_DESIGN_APPROVAL
|
||||
task_id: null
|
||||
action: "await_user_approval"
|
||||
|
|
@ -0,0 +1,323 @@
|
|||
# Workflow Versioning Schema
|
||||
# Links workflow sessions with task sessions and operations
|
||||
|
||||
# ============================================================================
|
||||
# WORKFLOW SESSION (Top Level)
|
||||
# ============================================================================
|
||||
workflow_session:
|
||||
# Unique version identifier
|
||||
version: string # v001, v002, v003...
|
||||
|
||||
# Feature being implemented
|
||||
feature: string
|
||||
|
||||
# Session metadata
|
||||
session_id: string # workflow_<timestamp>
|
||||
parent_version: string | null # If this is a continuation/fix
|
||||
|
||||
# Status
|
||||
status: pending | in_progress | completed | failed | rolled_back
|
||||
|
||||
# Timestamps
|
||||
started_at: timestamp
|
||||
completed_at: timestamp | null
|
||||
|
||||
# Approval records
|
||||
approvals:
|
||||
design:
|
||||
status: pending | approved | rejected
|
||||
approved_by: string | null
|
||||
approved_at: timestamp | null
|
||||
rejection_reason: string | null
|
||||
implementation:
|
||||
status: pending | approved | rejected
|
||||
approved_by: string | null
|
||||
approved_at: timestamp | null
|
||||
rejection_reason: string | null
|
||||
|
||||
# Linked task sessions
|
||||
task_sessions: [task_session_id]
|
||||
|
||||
# Aggregate summary
|
||||
summary:
|
||||
total_tasks: integer
|
||||
tasks_completed: integer
|
||||
entities_created: integer
|
||||
entities_updated: integer
|
||||
entities_deleted: integer
|
||||
files_created: integer
|
||||
files_updated: integer
|
||||
files_deleted: integer
|
||||
|
||||
# ============================================================================
|
||||
# TASK SESSION (Per Task)
|
||||
# ============================================================================
|
||||
task_session:
|
||||
# Unique identifier
|
||||
session_id: string # tasksession_<task_id>_<timestamp>
|
||||
|
||||
# Link to parent workflow
|
||||
workflow_version: string # v001
|
||||
|
||||
# Task reference
|
||||
task_id: string
|
||||
task_type: create | update | delete | refactor | test
|
||||
|
||||
# Agent info
|
||||
agent: frontend | backend | reviewer | architect
|
||||
|
||||
# Timestamps
|
||||
started_at: timestamp
|
||||
completed_at: timestamp | null
|
||||
duration_ms: integer | null
|
||||
|
||||
# Status
|
||||
status: pending | in_progress | review | approved | completed | failed | blocked
|
||||
|
||||
# Operations performed in this session
|
||||
operations: [operation]
|
||||
|
||||
# Review link (if reviewed)
|
||||
review_session: review_session | null
|
||||
|
||||
# Error tracking
|
||||
errors: [error_record]
|
||||
|
||||
# Retry info
|
||||
attempt_number: integer # 1, 2, 3...
|
||||
previous_attempts: [session_id]
|
||||
|
||||
# ============================================================================
|
||||
# OPERATION (Atomic Change)
|
||||
# ============================================================================
|
||||
operation:
|
||||
# Unique operation ID
|
||||
id: string # op_<timestamp>_<sequence>
|
||||
|
||||
# Operation type
|
||||
type: CREATE | UPDATE | DELETE | RENAME | MOVE
|
||||
|
||||
# Target
|
||||
target_type: file | entity | task | manifest
|
||||
target_id: string # entity_id or file path
|
||||
target_path: string | null # file path if applicable
|
||||
|
||||
# Change details
|
||||
changes:
|
||||
before: string | null # Previous state/content hash
|
||||
after: string | null # New state/content hash
|
||||
diff_summary: string # Human-readable summary
|
||||
|
||||
# Timestamp
|
||||
performed_at: timestamp
|
||||
|
||||
# Reversibility
|
||||
reversible: boolean
|
||||
rollback_data: object | null # Data needed to reverse
|
||||
|
||||
# ============================================================================
|
||||
# REVIEW SESSION
|
||||
# ============================================================================
|
||||
review_session:
|
||||
session_id: string # review_<task_id>_<timestamp>
|
||||
|
||||
# Links
|
||||
task_session_id: string
|
||||
workflow_version: string
|
||||
|
||||
# Reviewer
|
||||
reviewer: string # "reviewer" agent or user
|
||||
|
||||
# Timing
|
||||
started_at: timestamp
|
||||
completed_at: timestamp
|
||||
|
||||
# Decision
|
||||
decision: approved | rejected | needs_changes
|
||||
|
||||
# Checks performed
|
||||
checks:
|
||||
file_exists: pass | fail | skip
|
||||
manifest_compliance: pass | fail | skip
|
||||
code_quality: pass | fail | skip
|
||||
lint: pass | fail | skip
|
||||
build: pass | fail | skip
|
||||
tests: pass | fail | skip
|
||||
|
||||
# Feedback
|
||||
notes: string
|
||||
issues_found: [string]
|
||||
suggestions: [string]
|
||||
|
||||
# ============================================================================
|
||||
# ERROR RECORD
|
||||
# ============================================================================
|
||||
error_record:
|
||||
timestamp: timestamp
|
||||
phase: string # Which step failed
|
||||
error_type: string
|
||||
message: string
|
||||
stack_trace: string | null
|
||||
resolved: boolean
|
||||
resolution: string | null
|
||||
|
||||
# ============================================================================
|
||||
# VERSION INDEX (Quick Lookup)
|
||||
# ============================================================================
|
||||
version_index:
|
||||
versions:
|
||||
- version: v001
|
||||
feature: "User authentication"
|
||||
status: completed
|
||||
started_at: timestamp
|
||||
completed_at: timestamp
|
||||
tasks_count: 8
|
||||
operations_count: 15
|
||||
- version: v002
|
||||
feature: "Task filters"
|
||||
status: in_progress
|
||||
started_at: timestamp
|
||||
completed_at: null
|
||||
tasks_count: 5
|
||||
operations_count: 7
|
||||
|
||||
latest_version: v002
|
||||
total_versions: 2
|
||||
|
||||
# ============================================================================
|
||||
# TASK SESSION DIRECTORY STRUCTURE
|
||||
# ============================================================================
|
||||
task_session_directory:
|
||||
description: "Each task session has its own directory with full context"
|
||||
path_pattern: ".workflow/versions/{version}/task_sessions/{task_id}/"
|
||||
|
||||
files:
|
||||
session.yml:
|
||||
description: "Task session metadata (existing schema)"
|
||||
schema: task_session
|
||||
|
||||
task.yml:
|
||||
description: "Snapshot of task definition at execution time"
|
||||
fields:
|
||||
id: string
|
||||
type: create | update | delete | refactor | test
|
||||
title: string
|
||||
agent: frontend | backend | reviewer | architect
|
||||
status_at_snapshot: string
|
||||
entity_ids: [string]
|
||||
file_paths: [string]
|
||||
dependencies: [string]
|
||||
description: string
|
||||
acceptance_criteria: [string]
|
||||
snapshotted_at: timestamp
|
||||
source_path: string
|
||||
|
||||
operations.log:
|
||||
description: "Chronological audit trail of all operations"
|
||||
format: text
|
||||
entry_pattern: "[{timestamp}] {operation_type} {target_type}: {target_id} ({path})"
|
||||
|
||||
# ============================================================================
|
||||
# EXAMPLE: Complete Workflow Session
|
||||
# ============================================================================
|
||||
example_workflow_session:
|
||||
version: v001
|
||||
feature: "User authentication with OAuth"
|
||||
session_id: workflow_20250116_143022
|
||||
parent_version: null
|
||||
status: completed
|
||||
started_at: "2025-01-16T14:30:22Z"
|
||||
completed_at: "2025-01-16T15:45:00Z"
|
||||
|
||||
approvals:
|
||||
design:
|
||||
status: approved
|
||||
approved_by: user
|
||||
approved_at: "2025-01-16T14:45:00Z"
|
||||
rejection_reason: null
|
||||
implementation:
|
||||
status: approved
|
||||
approved_by: user
|
||||
approved_at: "2025-01-16T15:40:00Z"
|
||||
rejection_reason: null
|
||||
|
||||
task_sessions:
|
||||
- tasksession_task_create_LoginPage_20250116_144501
|
||||
- tasksession_task_create_AuthAPI_20250116_145001
|
||||
- tasksession_task_update_Header_20250116_150001
|
||||
|
||||
summary:
|
||||
total_tasks: 3
|
||||
tasks_completed: 3
|
||||
entities_created: 2
|
||||
entities_updated: 1
|
||||
entities_deleted: 0
|
||||
files_created: 3
|
||||
files_updated: 2
|
||||
files_deleted: 0
|
||||
|
||||
example_task_session:
|
||||
session_id: tasksession_task_create_LoginPage_20250116_144501
|
||||
workflow_version: v001
|
||||
task_id: task_create_LoginPage
|
||||
task_type: create
|
||||
agent: frontend
|
||||
started_at: "2025-01-16T14:45:01Z"
|
||||
completed_at: "2025-01-16T14:55:00Z"
|
||||
duration_ms: 599000
|
||||
status: completed
|
||||
|
||||
operations:
|
||||
- id: op_20250116_144502_001
|
||||
type: CREATE
|
||||
target_type: file
|
||||
target_id: page_login
|
||||
target_path: app/login/page.tsx
|
||||
changes:
|
||||
before: null
|
||||
after: "sha256:abc123..."
|
||||
diff_summary: "Created login page with email/password form"
|
||||
performed_at: "2025-01-16T14:45:02Z"
|
||||
reversible: true
|
||||
rollback_data:
|
||||
action: delete_file
|
||||
path: app/login/page.tsx
|
||||
|
||||
- id: op_20250116_144503_002
|
||||
type: UPDATE
|
||||
target_type: manifest
|
||||
target_id: project_manifest
|
||||
target_path: project_manifest.json
|
||||
changes:
|
||||
before: "sha256:def456..."
|
||||
after: "sha256:ghi789..."
|
||||
diff_summary: "Added page_login entity, set status to IMPLEMENTED"
|
||||
performed_at: "2025-01-16T14:45:03Z"
|
||||
reversible: true
|
||||
rollback_data:
|
||||
action: restore_content
|
||||
content_hash: "sha256:def456..."
|
||||
|
||||
review_session:
|
||||
session_id: review_task_create_LoginPage_20250116_145501
|
||||
task_session_id: tasksession_task_create_LoginPage_20250116_144501
|
||||
workflow_version: v001
|
||||
reviewer: reviewer
|
||||
started_at: "2025-01-16T14:55:01Z"
|
||||
completed_at: "2025-01-16T14:58:00Z"
|
||||
decision: approved
|
||||
checks:
|
||||
file_exists: pass
|
||||
manifest_compliance: pass
|
||||
code_quality: pass
|
||||
lint: pass
|
||||
build: pass
|
||||
tests: skip
|
||||
notes: "Login page implementation matches manifest spec"
|
||||
issues_found: []
|
||||
suggestions:
|
||||
- "Consider adding loading state for form submission"
|
||||
|
||||
errors: []
|
||||
attempt_number: 1
|
||||
previous_attempts: []
|
||||
|
|
@ -0,0 +1,274 @@
|
|||
# Task Session Migration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide explains how to migrate task sessions from the old flat file structure to the new directory-based structure.
|
||||
|
||||
## Background
|
||||
|
||||
### Old Structure (Flat Files)
|
||||
```
|
||||
.workflow/versions/v001/task_sessions/
|
||||
├── task_design.yml
|
||||
├── task_implementation.yml
|
||||
└── task_review.yml
|
||||
```
|
||||
|
||||
### New Structure (Directories)
|
||||
```
|
||||
.workflow/versions/v001/task_sessions/
|
||||
├── task_design/
|
||||
│ ├── session.yml # Session data (execution info)
|
||||
│ ├── task.yml # Task snapshot (definition at execution time)
|
||||
│ └── operations.log # Human-readable operation log
|
||||
├── task_implementation/
|
||||
│ ├── session.yml
|
||||
│ ├── task.yml
|
||||
│ └── operations.log
|
||||
└── task_review/
|
||||
├── session.yml
|
||||
├── task.yml
|
||||
└── operations.log
|
||||
```
|
||||
|
||||
## Benefits of New Structure
|
||||
|
||||
1. **Better Organization**: Each task session has its own directory
|
||||
2. **Snapshot Preservation**: Task definitions are captured at execution time
|
||||
3. **Human-Readable Logs**: Operations log provides easy-to-read history
|
||||
4. **Extensibility**: Easy to add attachments, artifacts, or outputs per task
|
||||
5. **Backwards Compatible**: Old code can still read from either structure
|
||||
|
||||
## Migration Script
|
||||
|
||||
### Location
|
||||
```
|
||||
skills/guardrail-orchestrator/scripts/migrate_task_sessions.py
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
#### Dry Run (Recommended First Step)
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/migrate_task_sessions.py --dry-run
|
||||
```
|
||||
|
||||
This will:
|
||||
- Find all flat task session files
|
||||
- Report what would be migrated
|
||||
- Show actions that would be taken
|
||||
- **NOT make any changes**
|
||||
|
||||
#### Live Migration
|
||||
```bash
|
||||
python3 skills/guardrail-orchestrator/scripts/migrate_task_sessions.py
|
||||
```
|
||||
|
||||
This will:
|
||||
- Create directory for each task session
|
||||
- Move session data to `session.yml`
|
||||
- Create `task.yml` snapshot
|
||||
- Generate `operations.log`
|
||||
- Delete original flat files
|
||||
|
||||
## Migration Process
|
||||
|
||||
### What the Script Does
|
||||
|
||||
For each flat task session file (e.g., `task_design.yml`):
|
||||
|
||||
1. **Create Directory**: `task_sessions/task_design/`
|
||||
|
||||
2. **Move Session Data**:
|
||||
- Read original `task_design.yml`
|
||||
- Save to `task_design/session.yml`
|
||||
- Delete original file
|
||||
|
||||
3. **Create Task Snapshot**:
|
||||
- Look for `tasks/task_design.yml`
|
||||
- If found: Copy and add snapshot metadata
|
||||
- If not found: Create minimal task.yml from session data
|
||||
- Save to `task_design/task.yml`
|
||||
|
||||
4. **Create Operations Log**:
|
||||
- Initialize `task_design/operations.log`
|
||||
- Add migration note
|
||||
- If session has operations array, convert to log format
|
||||
- Human-readable format with timestamps
|
||||
|
||||
### Task Snapshot Metadata
|
||||
|
||||
When a task definition is found, these fields are added:
|
||||
```yaml
|
||||
snapshotted_at: '2025-12-16T12:00:00'
|
||||
source_path: 'tasks/task_design.yml'
|
||||
status_at_snapshot: 'completed'
|
||||
migration_note: 'Created during migration from flat file structure'
|
||||
```
|
||||
|
||||
### Operations Log Format
|
||||
|
||||
```
|
||||
# Operations Log for task_design
|
||||
# Migrated: 2025-12-16T12:00:00
|
||||
# Format: [timestamp] OPERATION target_type: target_id (path)
|
||||
======================================================================
|
||||
|
||||
[2025-12-16T12:00:00] MIGRATION: Converted from flat file structure
|
||||
|
||||
# Historical operations from session data:
|
||||
[2025-12-16T11:00:00] CREATE file: auth.ts (app/lib/auth.ts)
|
||||
Summary: Created authentication module
|
||||
|
||||
[2025-12-16T11:15:00] UPDATE entity: User (app/lib/types.ts)
|
||||
Summary: Added email field to User type
|
||||
```
|
||||
|
||||
## Migration Results
|
||||
|
||||
### Success Output
|
||||
```
|
||||
======================================================================
|
||||
Migration Summary
|
||||
======================================================================
|
||||
Total files processed: 3
|
||||
Successful migrations: 3
|
||||
Failed migrations: 0
|
||||
|
||||
Migration completed successfully!
|
||||
|
||||
Next steps:
|
||||
1. Verify migrated files in .workflow/versions/*/task_sessions/
|
||||
2. Check that each task has session.yml, task.yml, and operations.log
|
||||
3. Test the system to ensure compatibility
|
||||
```
|
||||
|
||||
### Dry Run Output
|
||||
```
|
||||
Processing: v001/task_design.yml
|
||||
----------------------------------------------------------------------
|
||||
Would create directory: .workflow/versions/v001/task_sessions/task_design
|
||||
Would move task_design.yml to .workflow/versions/v001/task_sessions/task_design/session.yml
|
||||
Would create .workflow/versions/v001/task_sessions/task_design/task.yml (if source exists)
|
||||
Would create .workflow/versions/v001/task_sessions/task_design/operations.log
|
||||
|
||||
This was a DRY RUN. No files were modified.
|
||||
Run without --dry-run to perform the migration.
|
||||
```
|
||||
|
||||
## Verification Steps
|
||||
|
||||
After migration, verify the structure:
|
||||
|
||||
```bash
|
||||
# Check directory structure
|
||||
ls -la .workflow/versions/v001/task_sessions/task_design/
|
||||
|
||||
# Should show:
|
||||
# session.yml
|
||||
# task.yml
|
||||
# operations.log
|
||||
|
||||
# Verify session data
|
||||
cat .workflow/versions/v001/task_sessions/task_design/session.yml
|
||||
|
||||
# Verify task snapshot
|
||||
cat .workflow/versions/v001/task_sessions/task_design/task.yml
|
||||
|
||||
# Check operations log
|
||||
cat .workflow/versions/v001/task_sessions/task_design/operations.log
|
||||
```
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
The `version_manager.py` module includes backwards-compatible loading:
|
||||
|
||||
```python
|
||||
def load_task_session(version: str, task_id: str) -> Optional[dict]:
|
||||
"""Load a task session from directory or flat file (backwards compatible)."""
|
||||
# Try new directory structure first
|
||||
session_dir = get_version_dir(version) / 'task_sessions' / task_id
|
||||
session_path = session_dir / 'session.yml'
|
||||
|
||||
if session_path.exists():
|
||||
return load_yaml(str(session_path))
|
||||
|
||||
# Fallback to old flat file structure
|
||||
old_path = get_version_dir(version) / 'task_sessions' / f'{task_id}.yml'
|
||||
if old_path.exists():
|
||||
return load_yaml(str(old_path))
|
||||
|
||||
return None
|
||||
```
|
||||
|
||||
This means:
|
||||
- New code works with both structures
|
||||
- No breaking changes for existing workflows
|
||||
- Migration can be done gradually
|
||||
- Rollback is possible if needed
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No Files Found
|
||||
If the script reports "No flat task session files found":
|
||||
- Check that `.workflow/versions/` exists
|
||||
- Verify that task sessions are in expected location
|
||||
- Confirm files have `.yml` or `.yaml` extension
|
||||
- May indicate all sessions are already migrated
|
||||
|
||||
### Task File Not Found
|
||||
If `tasks/task_id.yml` doesn't exist:
|
||||
- Script creates minimal task.yml from session data
|
||||
- Warning is logged but migration continues
|
||||
- Check `task.yml` has `migration_note` field
|
||||
|
||||
### Migration Errors
|
||||
If migration fails:
|
||||
- Review error message in output
|
||||
- Check file permissions
|
||||
- Verify disk space
|
||||
- Try dry-run mode to diagnose
|
||||
|
||||
### Rollback (If Needed)
|
||||
To rollback a migration:
|
||||
1. Stop any running workflows
|
||||
2. For each migrated directory:
|
||||
```bash
|
||||
# Copy session.yml back to flat file
|
||||
cp .workflow/versions/v001/task_sessions/task_design/session.yml \
|
||||
.workflow/versions/v001/task_sessions/task_design.yml
|
||||
|
||||
# Remove directory
|
||||
rm -rf .workflow/versions/v001/task_sessions/task_design/
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always dry-run first**: Use `--dry-run` to preview changes
|
||||
2. **Backup before migration**: Copy `.workflow/` directory
|
||||
3. **Migrate per version**: Test one version before migrating all
|
||||
4. **Verify after migration**: Check files and run system tests
|
||||
5. **Keep old backups**: Don't delete backups immediately
|
||||
|
||||
## Integration with Workflow System
|
||||
|
||||
After migration, all workflow operations work seamlessly:
|
||||
|
||||
```python
|
||||
# Start task session (creates directory structure)
|
||||
session = create_workflow_session("new feature", None)
|
||||
task_session = create_task_session(session, "task_api", "create", "backend")
|
||||
|
||||
# Load task session (works with both structures)
|
||||
task = load_task_session("v001", "task_design")
|
||||
|
||||
# Log operations (appends to operations.log)
|
||||
log_operation(task, "CREATE", "file", "api.ts", target_path="app/api/api.ts")
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- `version_manager.py`: Core versioning system
|
||||
- `workflow_manager.py`: Workflow orchestration
|
||||
- `.workflow/operations.log`: Global operations log
|
||||
- `.workflow/index.yml`: Version index
|
||||
|
|
@ -0,0 +1,486 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Analyze codebase and generate project manifest from existing code."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
def find_files(base_path: str, pattern: str) -> list[str]:
|
||||
"""Find files matching a glob pattern."""
|
||||
base = Path(base_path)
|
||||
return [str(p.relative_to(base)) for p in base.glob(pattern)]
|
||||
|
||||
|
||||
def read_file(filepath: str) -> str:
|
||||
"""Read file contents."""
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def extract_component_name(filepath: str) -> str:
|
||||
"""Extract component name from file path."""
|
||||
name = Path(filepath).stem
|
||||
return name
|
||||
|
||||
|
||||
def to_snake_case(name: str) -> str:
|
||||
"""Convert PascalCase to snake_case."""
|
||||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
||||
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
||||
|
||||
|
||||
def extract_props_from_content(content: str) -> dict:
|
||||
"""Extract props interface from component content."""
|
||||
props = {}
|
||||
|
||||
# Look for interface Props or type Props
|
||||
interface_match = re.search(
|
||||
r'(?:interface|type)\s+\w*Props\w*\s*(?:=\s*)?\{([^}]+)\}',
|
||||
content,
|
||||
re.DOTALL
|
||||
)
|
||||
|
||||
if interface_match:
|
||||
props_block = interface_match.group(1)
|
||||
# Parse individual props
|
||||
prop_matches = re.findall(
|
||||
r'(\w+)(\?)?:\s*([^;,\n]+)',
|
||||
props_block
|
||||
)
|
||||
for name, optional, prop_type in prop_matches:
|
||||
props[name] = {
|
||||
"type": prop_type.strip(),
|
||||
"optional": bool(optional)
|
||||
}
|
||||
|
||||
return props
|
||||
|
||||
|
||||
def extract_imports(content: str) -> list[str]:
|
||||
"""Extract component imports from file."""
|
||||
imports = []
|
||||
|
||||
# Look for imports from components directory
|
||||
import_matches = re.findall(
|
||||
r"import\s+\{?\s*([^}]+)\s*\}?\s+from\s+['\"]\.\.?/components/(\w+)['\"]",
|
||||
content
|
||||
)
|
||||
|
||||
for imported, component in import_matches:
|
||||
imports.append(component)
|
||||
|
||||
# Also check for direct component imports
|
||||
direct_imports = re.findall(
|
||||
r"import\s+(\w+)\s+from\s+['\"]\.\.?/components/(\w+)['\"]",
|
||||
content
|
||||
)
|
||||
|
||||
for imported, component in direct_imports:
|
||||
imports.append(component)
|
||||
|
||||
return list(set(imports))
|
||||
|
||||
|
||||
def extract_api_methods(content: str) -> list[str]:
|
||||
"""Extract HTTP methods from API route file."""
|
||||
methods = []
|
||||
method_patterns = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']
|
||||
|
||||
for method in method_patterns:
|
||||
if re.search(rf'export\s+(?:async\s+)?function\s+{method}\s*\(', content):
|
||||
methods.append(method)
|
||||
|
||||
return methods
|
||||
|
||||
|
||||
def extract_fetch_calls(content: str) -> list[str]:
|
||||
"""Extract API fetch calls from content."""
|
||||
apis = []
|
||||
|
||||
# Look for fetch('/api/...') patterns - handle static paths
|
||||
fetch_matches = re.findall(
|
||||
r"fetch\s*\(\s*['\"`]/api/([^'\"`\?\$\{]+)",
|
||||
content
|
||||
)
|
||||
apis.extend(fetch_matches)
|
||||
|
||||
# Look for fetch(`/api/tasks`) or similar template literals with static paths
|
||||
template_matches = re.findall(
|
||||
r"fetch\s*\(\s*`/api/(\w+)`",
|
||||
content
|
||||
)
|
||||
apis.extend(template_matches)
|
||||
|
||||
# Clean up: remove trailing slashes and normalize
|
||||
cleaned = []
|
||||
for api in apis:
|
||||
api = api.rstrip('/')
|
||||
if api and not api.startswith('$'):
|
||||
cleaned.append(api)
|
||||
|
||||
return list(set(cleaned))
|
||||
|
||||
|
||||
def extract_types_from_db(content: str) -> dict:
|
||||
"""Extract type definitions from db.ts or similar."""
|
||||
types = {}
|
||||
|
||||
# Extract interfaces
|
||||
interface_matches = re.findall(
|
||||
r'export\s+interface\s+(\w+)\s*\{([^}]+)\}',
|
||||
content,
|
||||
re.DOTALL
|
||||
)
|
||||
|
||||
for name, body in interface_matches:
|
||||
fields = {}
|
||||
field_matches = re.findall(r'(\w+)(\?)?:\s*([^;,\n]+)', body)
|
||||
for field_name, optional, field_type in field_matches:
|
||||
fields[field_name] = field_type.strip()
|
||||
types[name] = fields
|
||||
|
||||
# Extract type aliases
|
||||
type_matches = re.findall(
|
||||
r"export\s+type\s+(\w+)\s*=\s*([^;]+);",
|
||||
content
|
||||
)
|
||||
|
||||
for name, type_def in type_matches:
|
||||
types[name] = type_def.strip()
|
||||
|
||||
return types
|
||||
|
||||
|
||||
def path_to_route(filepath: str) -> str:
|
||||
"""Convert file path to route path."""
|
||||
# Remove app/ prefix and page.tsx suffix
|
||||
route = filepath.replace('app/', '').replace('/page.tsx', '').replace('page.tsx', '')
|
||||
|
||||
if route == '' or route == '/':
|
||||
return '/'
|
||||
|
||||
# Handle dynamic segments [id] -> [id]
|
||||
route = re.sub(r'\[([^\]]+)\]', r'[\1]', route)
|
||||
|
||||
# Ensure starts with /
|
||||
if not route.startswith('/'):
|
||||
route = '/' + route
|
||||
|
||||
return route
|
||||
|
||||
|
||||
def analyze_pages(base_path: str) -> list[dict]:
|
||||
"""Analyze all page files."""
|
||||
pages = []
|
||||
page_files = find_files(base_path, 'app/**/page.tsx')
|
||||
|
||||
for filepath in page_files:
|
||||
full_path = os.path.join(base_path, filepath)
|
||||
content = read_file(full_path)
|
||||
|
||||
route = path_to_route(filepath)
|
||||
|
||||
# Generate page ID
|
||||
if route == '/' or filepath == 'app/page.tsx':
|
||||
page_id = 'page_home'
|
||||
name = 'Home'
|
||||
route = '/'
|
||||
else:
|
||||
name = route.strip('/').replace('/', '_').replace('[', '').replace(']', '')
|
||||
page_id = f'page_{name}'
|
||||
|
||||
# Extract component imports
|
||||
components = extract_imports(content)
|
||||
comp_ids = [f"comp_{to_snake_case(c)}" for c in components]
|
||||
|
||||
# Extract API dependencies
|
||||
api_calls = extract_fetch_calls(content)
|
||||
api_ids = [f"api_{a.replace('/', '_')}" for a in api_calls]
|
||||
|
||||
pages.append({
|
||||
"id": page_id,
|
||||
"path": route,
|
||||
"file_path": filepath,
|
||||
"status": "IMPLEMENTED",
|
||||
"description": f"Page at {route}",
|
||||
"components": comp_ids,
|
||||
"data_dependencies": api_ids
|
||||
})
|
||||
|
||||
return pages
|
||||
|
||||
|
||||
def analyze_components(base_path: str) -> list[dict]:
|
||||
"""Analyze all component files."""
|
||||
components = []
|
||||
component_files = find_files(base_path, 'app/components/*.tsx')
|
||||
|
||||
for filepath in component_files:
|
||||
full_path = os.path.join(base_path, filepath)
|
||||
content = read_file(full_path)
|
||||
|
||||
name = extract_component_name(filepath)
|
||||
comp_id = f"comp_{to_snake_case(name)}"
|
||||
|
||||
# Extract props
|
||||
props = extract_props_from_content(content)
|
||||
|
||||
components.append({
|
||||
"id": comp_id,
|
||||
"name": name,
|
||||
"file_path": filepath,
|
||||
"status": "IMPLEMENTED",
|
||||
"description": f"{name} component",
|
||||
"props": props
|
||||
})
|
||||
|
||||
return components
|
||||
|
||||
|
||||
def analyze_apis(base_path: str) -> list[dict]:
|
||||
"""Analyze all API route files."""
|
||||
apis = []
|
||||
api_files = find_files(base_path, 'app/api/**/route.ts')
|
||||
|
||||
for filepath in api_files:
|
||||
full_path = os.path.join(base_path, filepath)
|
||||
content = read_file(full_path)
|
||||
|
||||
# Extract path from file location
|
||||
path = '/' + filepath.replace('app/', '').replace('/route.ts', '')
|
||||
|
||||
# Extract HTTP methods
|
||||
methods = extract_api_methods(content)
|
||||
|
||||
for method in methods:
|
||||
# Generate action name from method
|
||||
action_map = {
|
||||
'GET': 'list' if '[' not in path else 'get',
|
||||
'POST': 'create',
|
||||
'PUT': 'update',
|
||||
'DELETE': 'delete',
|
||||
'PATCH': 'patch'
|
||||
}
|
||||
action = action_map.get(method, method.lower())
|
||||
|
||||
# Generate resource name from path
|
||||
resource = path.replace('/api/', '').replace('/', '_').replace('[', '').replace(']', '')
|
||||
if not resource:
|
||||
resource = 'root'
|
||||
|
||||
api_id = f"api_{action}_{resource}"
|
||||
|
||||
apis.append({
|
||||
"id": api_id,
|
||||
"path": path,
|
||||
"method": method,
|
||||
"file_path": filepath,
|
||||
"status": "IMPLEMENTED",
|
||||
"description": f"{method} {path}",
|
||||
"request": {},
|
||||
"response": {
|
||||
"type": "object",
|
||||
"description": "Response data"
|
||||
}
|
||||
})
|
||||
|
||||
return apis
|
||||
|
||||
|
||||
def analyze_database(base_path: str) -> tuple[list[dict], dict]:
|
||||
"""Analyze database/type files."""
|
||||
tables = []
|
||||
types = {}
|
||||
|
||||
# Check for db.ts file
|
||||
db_path = os.path.join(base_path, 'app/lib/db.ts')
|
||||
if os.path.exists(db_path):
|
||||
content = read_file(db_path)
|
||||
types = extract_types_from_db(content)
|
||||
|
||||
# Look for table/collection definitions
|
||||
if 'tasks' in content.lower():
|
||||
tables.append({
|
||||
"id": "table_tasks",
|
||||
"name": "tasks",
|
||||
"file_path": "app/lib/db.ts",
|
||||
"status": "IMPLEMENTED",
|
||||
"description": "Tasks storage",
|
||||
"columns": types.get('Task', {})
|
||||
})
|
||||
|
||||
return tables, types
|
||||
|
||||
|
||||
def build_dependencies(pages: list, components: list, apis: list) -> dict:
|
||||
"""Build dependency mappings."""
|
||||
component_to_page = {}
|
||||
api_to_component = {}
|
||||
|
||||
# Build component to page mapping
|
||||
for page in pages:
|
||||
for comp_id in page.get('components', []):
|
||||
if comp_id not in component_to_page:
|
||||
component_to_page[comp_id] = []
|
||||
component_to_page[comp_id].append(page['id'])
|
||||
|
||||
# API to component would require deeper analysis
|
||||
# For now, we'll leave it based on page dependencies
|
||||
|
||||
return {
|
||||
"component_to_page": component_to_page,
|
||||
"api_to_component": {},
|
||||
"table_to_api": {}
|
||||
}
|
||||
|
||||
|
||||
def generate_manifest(
|
||||
base_path: str,
|
||||
project_name: Optional[str] = None
|
||||
) -> dict:
|
||||
"""Generate complete project manifest."""
|
||||
|
||||
# Determine project name
|
||||
if not project_name:
|
||||
# Try to get from package.json
|
||||
pkg_path = os.path.join(base_path, 'package.json')
|
||||
if os.path.exists(pkg_path):
|
||||
try:
|
||||
with open(pkg_path) as f:
|
||||
pkg = json.load(f)
|
||||
project_name = pkg.get('name', Path(base_path).name)
|
||||
except Exception:
|
||||
project_name = Path(base_path).name
|
||||
else:
|
||||
project_name = Path(base_path).name
|
||||
|
||||
# Analyze codebase
|
||||
pages = analyze_pages(base_path)
|
||||
components = analyze_components(base_path)
|
||||
apis = analyze_apis(base_path)
|
||||
tables, types = analyze_database(base_path)
|
||||
dependencies = build_dependencies(pages, components, apis)
|
||||
|
||||
now = datetime.now().isoformat()
|
||||
|
||||
manifest = {
|
||||
"project": {
|
||||
"name": project_name,
|
||||
"version": "1.0.0",
|
||||
"created_at": now,
|
||||
"description": f"Project manifest for {project_name}"
|
||||
},
|
||||
"state": {
|
||||
"current_phase": "IMPLEMENTATION_PHASE",
|
||||
"approval_status": {
|
||||
"manifest_approved": True,
|
||||
"approved_by": "analyzer",
|
||||
"approved_at": now
|
||||
},
|
||||
"revision_history": [
|
||||
{
|
||||
"action": "MANIFEST_GENERATED",
|
||||
"timestamp": now,
|
||||
"details": "Generated from existing codebase analysis"
|
||||
}
|
||||
]
|
||||
},
|
||||
"entities": {
|
||||
"pages": pages,
|
||||
"components": components,
|
||||
"api_endpoints": apis,
|
||||
"database_tables": tables
|
||||
},
|
||||
"dependencies": dependencies,
|
||||
"types": types
|
||||
}
|
||||
|
||||
return manifest
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Analyze codebase and generate project manifest'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--path',
|
||||
default='.',
|
||||
help='Path to project root'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--name',
|
||||
help='Project name (defaults to package.json name or directory name)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--output',
|
||||
default='project_manifest.json',
|
||||
help='Output file path'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help='Print manifest without writing to file'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--force',
|
||||
action='store_true',
|
||||
help='Overwrite existing manifest'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
base_path = os.path.abspath(args.path)
|
||||
output_path = os.path.join(base_path, args.output)
|
||||
|
||||
# Check for existing manifest
|
||||
if os.path.exists(output_path) and not args.force and not args.dry_run:
|
||||
print(f"Error: {args.output} already exists. Use --force to overwrite.")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Analyzing codebase at: {base_path}")
|
||||
print()
|
||||
|
||||
# Generate manifest
|
||||
manifest = generate_manifest(base_path, args.name)
|
||||
|
||||
# Count entities
|
||||
pages = len(manifest['entities']['pages'])
|
||||
components = len(manifest['entities']['components'])
|
||||
apis = len(manifest['entities']['api_endpoints'])
|
||||
tables = len(manifest['entities']['database_tables'])
|
||||
|
||||
if args.dry_run:
|
||||
print(json.dumps(manifest, indent=2))
|
||||
else:
|
||||
with open(output_path, 'w') as f:
|
||||
json.dump(manifest, f, indent=2)
|
||||
print(f"Manifest written to: {output_path}")
|
||||
|
||||
print()
|
||||
print("╔══════════════════════════════════════════════════════════════╗")
|
||||
print("║ 📊 MANIFEST GENERATED ║")
|
||||
print("╠══════════════════════════════════════════════════════════════╣")
|
||||
print(f"║ Project: {manifest['project']['name']:<51} ║")
|
||||
print("╠══════════════════════════════════════════════════════════════╣")
|
||||
print("║ ENTITIES DISCOVERED ║")
|
||||
print(f"║ 📄 Pages: {pages:<43} ║")
|
||||
print(f"║ 🧩 Components: {components:<43} ║")
|
||||
print(f"║ 🔌 APIs: {apis:<43} ║")
|
||||
print(f"║ 🗄️ Tables: {tables:<43} ║")
|
||||
print("╠══════════════════════════════════════════════════════════════╣")
|
||||
print("║ Status: All entities marked as IMPLEMENTED ║")
|
||||
print("║ Phase: IMPLEMENTATION_PHASE ║")
|
||||
print("╚══════════════════════════════════════════════════════════════╝")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,615 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate API Contract and Shared Types from Design Document
|
||||
|
||||
This script:
|
||||
1. Reads the design_document.yml
|
||||
2. Extracts all API endpoints and their types
|
||||
3. Generates api_contract.yml with strict typing
|
||||
4. Generates app/types/api.ts with shared TypeScript interfaces
|
||||
|
||||
Both frontend and backend agents MUST use these generated files
|
||||
to ensure contract compliance.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any, Optional, Set
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
yaml = None
|
||||
|
||||
|
||||
def load_yaml(path: Path) -> Dict:
|
||||
"""Load YAML file."""
|
||||
if yaml:
|
||||
with open(path) as f:
|
||||
return yaml.safe_load(f)
|
||||
else:
|
||||
# Fallback: simple YAML parser for basic cases
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
# Try JSON first (YAML is superset of JSON)
|
||||
try:
|
||||
return json.loads(content)
|
||||
except:
|
||||
print(f"Warning: yaml module not available, using basic parser", file=sys.stderr)
|
||||
return {}
|
||||
|
||||
|
||||
def save_yaml(data: Dict, path: Path) -> None:
|
||||
"""Save data as YAML."""
|
||||
if yaml:
|
||||
with open(path, 'w') as f:
|
||||
yaml.dump(data, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
||||
else:
|
||||
# Fallback: JSON format
|
||||
with open(path, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
|
||||
def ts_type_from_field(field: Dict) -> str:
|
||||
"""Convert design document field type to TypeScript type."""
|
||||
type_map = {
|
||||
'string': 'string',
|
||||
'text': 'string',
|
||||
'integer': 'number',
|
||||
'float': 'number',
|
||||
'decimal': 'number',
|
||||
'boolean': 'boolean',
|
||||
'datetime': 'Date',
|
||||
'date': 'Date',
|
||||
'uuid': 'string',
|
||||
'json': 'Record<string, unknown>',
|
||||
'array': 'unknown[]',
|
||||
}
|
||||
|
||||
field_type = field.get('type', 'string')
|
||||
|
||||
# Handle enum type
|
||||
if field_type == 'enum':
|
||||
enum_values = field.get('enum_values', [])
|
||||
if enum_values:
|
||||
return ' | '.join([f"'{v}'" for v in enum_values])
|
||||
return 'string'
|
||||
|
||||
return type_map.get(field_type, 'unknown')
|
||||
|
||||
|
||||
def generate_type_from_model(model: Dict) -> Dict:
|
||||
"""Generate TypeScript type definition from model."""
|
||||
type_id = f"type_{model['name']}"
|
||||
properties = []
|
||||
|
||||
for field in model.get('fields', []):
|
||||
# Skip internal fields like password_hash
|
||||
if field['name'].endswith('_hash'):
|
||||
continue
|
||||
|
||||
constraints = field.get('constraints', [])
|
||||
required = 'not_null' in constraints or 'primary_key' in constraints
|
||||
|
||||
properties.append({
|
||||
'name': to_camel_case(field['name']),
|
||||
'type': ts_type_from_field(field),
|
||||
'required': required,
|
||||
'description': field.get('description', ''),
|
||||
})
|
||||
|
||||
return {
|
||||
'id': type_id,
|
||||
'name': model['name'],
|
||||
'definition': {
|
||||
'type': 'object',
|
||||
'properties': properties,
|
||||
},
|
||||
'used_by': {
|
||||
'models': [model['id']],
|
||||
'responses': [],
|
||||
'requests': [],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def generate_request_type(endpoint: Dict) -> Optional[Dict]:
|
||||
"""Generate request body type from endpoint definition."""
|
||||
request_body = endpoint.get('request_body', {})
|
||||
if not request_body:
|
||||
return None
|
||||
|
||||
schema = request_body.get('schema', {})
|
||||
if not schema:
|
||||
return None
|
||||
|
||||
# Generate type name from endpoint
|
||||
parts = endpoint['id'].replace('api_', '').split('_')
|
||||
type_name = ''.join([p.capitalize() for p in parts]) + 'Request'
|
||||
type_id = f"type_{type_name}"
|
||||
|
||||
properties = []
|
||||
for prop in schema.get('properties', []):
|
||||
properties.append({
|
||||
'name': to_camel_case(prop['name']),
|
||||
'type': ts_type_from_field(prop),
|
||||
'required': prop.get('required', False),
|
||||
'description': prop.get('description', ''),
|
||||
'validation': ','.join(prop.get('validations', [])) if prop.get('validations') else None,
|
||||
})
|
||||
|
||||
return {
|
||||
'id': type_id,
|
||||
'name': type_name,
|
||||
'definition': {
|
||||
'type': 'object',
|
||||
'properties': properties,
|
||||
},
|
||||
'used_by': {
|
||||
'models': [],
|
||||
'responses': [],
|
||||
'requests': [endpoint['id']],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def generate_response_type(endpoint: Dict, models: Dict[str, Dict]) -> Optional[Dict]:
|
||||
"""Generate response type from endpoint definition - may reference model types."""
|
||||
responses = endpoint.get('responses', [])
|
||||
success_response = None
|
||||
|
||||
for resp in responses:
|
||||
status = resp.get('status', 0)
|
||||
if 200 <= status < 300:
|
||||
success_response = resp
|
||||
break
|
||||
|
||||
if not success_response:
|
||||
return None
|
||||
|
||||
# Check if this response references a model
|
||||
depends_on = endpoint.get('depends_on_models', [])
|
||||
if depends_on:
|
||||
# Response likely uses model type
|
||||
primary_model = depends_on[0]
|
||||
if primary_model in models:
|
||||
return None # Will use model type directly
|
||||
|
||||
# Generate custom response type
|
||||
schema = success_response.get('schema', {})
|
||||
if not schema or schema.get('type') != 'object':
|
||||
return None
|
||||
|
||||
parts = endpoint['id'].replace('api_', '').split('_')
|
||||
type_name = ''.join([p.capitalize() for p in parts]) + 'Response'
|
||||
type_id = f"type_{type_name}"
|
||||
|
||||
properties = []
|
||||
for prop in schema.get('properties', []):
|
||||
properties.append({
|
||||
'name': to_camel_case(prop['name']),
|
||||
'type': ts_type_from_field(prop),
|
||||
'required': True,
|
||||
'description': '',
|
||||
})
|
||||
|
||||
return {
|
||||
'id': type_id,
|
||||
'name': type_name,
|
||||
'definition': {
|
||||
'type': 'object',
|
||||
'properties': properties,
|
||||
},
|
||||
'used_by': {
|
||||
'models': [],
|
||||
'responses': [endpoint['id']],
|
||||
'requests': [],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def to_camel_case(snake_str: str) -> str:
|
||||
"""Convert snake_case to camelCase."""
|
||||
components = snake_str.split('_')
|
||||
return components[0] + ''.join(x.capitalize() for x in components[1:])
|
||||
|
||||
|
||||
def generate_endpoint_contract(endpoint: Dict, types: Dict[str, Dict], models: Dict[str, Dict]) -> Dict:
|
||||
"""Generate endpoint contract from design document endpoint."""
|
||||
# Determine request body type
|
||||
request_body = None
|
||||
if endpoint.get('request_body'):
|
||||
# Generate request type name
|
||||
parts = endpoint['id'].replace('api_', '').split('_')
|
||||
type_name = ''.join([p.capitalize() for p in parts]) + 'Request'
|
||||
request_body = {
|
||||
'type_id': f"type_{type_name}",
|
||||
'content_type': 'application/json',
|
||||
}
|
||||
|
||||
# Determine response type
|
||||
response_type_id = None
|
||||
is_array = False
|
||||
|
||||
responses = endpoint.get('responses', [])
|
||||
success_response = None
|
||||
for resp in responses:
|
||||
if 200 <= resp.get('status', 0) < 300:
|
||||
success_response = resp
|
||||
break
|
||||
|
||||
if success_response:
|
||||
# Check if referencing a model
|
||||
depends_on = endpoint.get('depends_on_models', [])
|
||||
if depends_on:
|
||||
model_id = depends_on[0]
|
||||
if model_id in models:
|
||||
model_name = models[model_id].get('name', model_id.replace('model_', '').capitalize())
|
||||
response_type_id = f"type_{model_name}"
|
||||
|
||||
# Check if response is array
|
||||
schema = success_response.get('schema', {})
|
||||
if schema.get('type') == 'array':
|
||||
is_array = True
|
||||
|
||||
if not response_type_id:
|
||||
parts = endpoint['id'].replace('api_', '').split('_')
|
||||
type_name = ''.join([p.capitalize() for p in parts]) + 'Response'
|
||||
response_type_id = f"type_{type_name}"
|
||||
|
||||
# Extract path params
|
||||
path_params = []
|
||||
for param in endpoint.get('path_params', []):
|
||||
path_params.append({
|
||||
'name': param['name'],
|
||||
'type': ts_type_from_field(param),
|
||||
'description': param.get('description', ''),
|
||||
})
|
||||
|
||||
# Extract query params
|
||||
query_params = []
|
||||
for param in endpoint.get('query_params', []):
|
||||
query_params.append({
|
||||
'name': param['name'],
|
||||
'type': ts_type_from_field(param),
|
||||
'required': param.get('required', False),
|
||||
'default': param.get('default'),
|
||||
'description': param.get('description', ''),
|
||||
})
|
||||
|
||||
# Build error responses
|
||||
error_responses = []
|
||||
for resp in responses:
|
||||
status = resp.get('status', 0)
|
||||
if status >= 400:
|
||||
error_responses.append({
|
||||
'status': status,
|
||||
'type_id': 'type_ApiError',
|
||||
'description': resp.get('description', ''),
|
||||
})
|
||||
|
||||
return {
|
||||
'id': endpoint['id'],
|
||||
'method': endpoint['method'],
|
||||
'path': endpoint['path'],
|
||||
'path_params': path_params,
|
||||
'query_params': query_params,
|
||||
'request_body': request_body,
|
||||
'response': {
|
||||
'success': {
|
||||
'status': success_response.get('status', 200) if success_response else 200,
|
||||
'type_id': response_type_id,
|
||||
'is_array': is_array,
|
||||
},
|
||||
'errors': error_responses,
|
||||
},
|
||||
'auth': endpoint.get('auth', {'required': False, 'roles': []}),
|
||||
'version': '1.0.0',
|
||||
}
|
||||
|
||||
|
||||
def generate_frontend_calls(pages: List[Dict], components: List[Dict], endpoints: Dict[str, Dict]) -> List[Dict]:
|
||||
"""Generate frontend call contracts from pages and components."""
|
||||
calls = []
|
||||
|
||||
# From pages
|
||||
for page in pages:
|
||||
for data_need in page.get('data_needs', []):
|
||||
api_id = data_need.get('api_id')
|
||||
if api_id and api_id in endpoints:
|
||||
calls.append({
|
||||
'id': f"call_{page['id']}_{api_id}",
|
||||
'source': {
|
||||
'entity_id': page['id'],
|
||||
'file_path': f"app{page['path']}/page.tsx",
|
||||
},
|
||||
'endpoint_id': api_id,
|
||||
'purpose': data_need.get('purpose', 'Load data'),
|
||||
'trigger': 'onLoad' if data_need.get('on_load') else 'onDemand',
|
||||
'request_mapping': {
|
||||
'from_props': [],
|
||||
'from_state': [],
|
||||
'from_form': [],
|
||||
},
|
||||
'response_handling': {
|
||||
'success_action': 'Update state',
|
||||
'error_action': 'Show error',
|
||||
},
|
||||
})
|
||||
|
||||
# From components
|
||||
for component in components:
|
||||
for api_id in component.get('uses_apis', []):
|
||||
if api_id in endpoints:
|
||||
endpoint = endpoints[api_id]
|
||||
method = endpoint.get('method', 'GET')
|
||||
trigger = 'onSubmit' if method in ['POST', 'PUT', 'PATCH'] else 'onDemand'
|
||||
|
||||
calls.append({
|
||||
'id': f"call_{component['id']}_{api_id}",
|
||||
'source': {
|
||||
'entity_id': component['id'],
|
||||
'file_path': f"app/components/{component['name']}.tsx",
|
||||
},
|
||||
'endpoint_id': api_id,
|
||||
'purpose': f"Call {api_id}",
|
||||
'trigger': trigger,
|
||||
'request_mapping': {
|
||||
'from_props': [],
|
||||
'from_state': [],
|
||||
'from_form': [],
|
||||
},
|
||||
'response_handling': {
|
||||
'success_action': 'Handle response',
|
||||
'error_action': 'Show error',
|
||||
},
|
||||
})
|
||||
|
||||
return calls
|
||||
|
||||
|
||||
def generate_backend_routes(endpoints: List[Dict]) -> List[Dict]:
|
||||
"""Generate backend route contracts from endpoints."""
|
||||
routes = []
|
||||
|
||||
for endpoint in endpoints:
|
||||
# Determine file path from endpoint path
|
||||
api_path = endpoint['path'].replace('/api/', '')
|
||||
# Handle dynamic segments like /users/:id
|
||||
parts = api_path.split('/')
|
||||
file_parts = []
|
||||
for part in parts:
|
||||
if part.startswith(':'):
|
||||
file_parts.append(f"[{part[1:]}]")
|
||||
else:
|
||||
file_parts.append(part)
|
||||
|
||||
file_path = f"app/api/{'/'.join(file_parts)}/route.ts"
|
||||
|
||||
routes.append({
|
||||
'id': f"route_{endpoint['method'].lower()}_{api_path.replace('/', '_')}",
|
||||
'endpoint_id': endpoint['id'],
|
||||
'file_path': file_path,
|
||||
'export_name': endpoint['method'],
|
||||
'uses_models': endpoint.get('depends_on_models', []),
|
||||
'uses_services': [],
|
||||
'must_validate': [],
|
||||
'must_authenticate': endpoint.get('auth', {}).get('required', False),
|
||||
'must_authorize': endpoint.get('auth', {}).get('roles', []),
|
||||
})
|
||||
|
||||
return routes
|
||||
|
||||
|
||||
def generate_typescript_types(types: List[Dict]) -> str:
|
||||
"""Generate TypeScript type definitions."""
|
||||
lines = [
|
||||
"// AUTO-GENERATED - DO NOT EDIT",
|
||||
"// Source: .workflow/versions/vXXX/contracts/api_contract.yml",
|
||||
f"// Generated: {datetime.now().isoformat()}",
|
||||
"",
|
||||
"// ============================================================================",
|
||||
"// Shared API Types",
|
||||
"// Both frontend and backend MUST import from this file",
|
||||
"// ============================================================================",
|
||||
"",
|
||||
]
|
||||
|
||||
# Add standard error types
|
||||
lines.extend([
|
||||
"// === Error Types ===",
|
||||
"",
|
||||
"export interface ApiError {",
|
||||
" error: string;",
|
||||
" message?: string;",
|
||||
" code?: string;",
|
||||
"}",
|
||||
"",
|
||||
"export interface ValidationError {",
|
||||
" error: string;",
|
||||
" details: string[];",
|
||||
"}",
|
||||
"",
|
||||
])
|
||||
|
||||
# Generate interfaces for each type
|
||||
lines.append("// === Domain Types ===")
|
||||
lines.append("")
|
||||
|
||||
for type_def in types:
|
||||
name = type_def['name']
|
||||
definition = type_def['definition']
|
||||
|
||||
if definition['type'] == 'object':
|
||||
lines.append(f"export interface {name} {{")
|
||||
for prop in definition.get('properties', []):
|
||||
optional = '' if prop.get('required') else '?'
|
||||
desc = prop.get('description', '')
|
||||
if desc:
|
||||
lines.append(f" /** {desc} */")
|
||||
lines.append(f" {prop['name']}{optional}: {prop['type']};")
|
||||
lines.append("}")
|
||||
lines.append("")
|
||||
|
||||
elif definition['type'] == 'enum':
|
||||
values = definition.get('enum_values', [])
|
||||
quoted_values = [f"'{v}'" for v in values]
|
||||
lines.append(f"export type {name} = {' | '.join(quoted_values)};")
|
||||
lines.append("")
|
||||
|
||||
elif definition['type'] == 'union':
|
||||
members = definition.get('union_members', [])
|
||||
lines.append(f"export type {name} = {' | '.join(members)};")
|
||||
lines.append("")
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def generate_api_paths(endpoints: List[Dict]) -> str:
|
||||
"""Generate API path constants for type-safe calls."""
|
||||
lines = [
|
||||
"",
|
||||
"// === API Paths ===",
|
||||
"",
|
||||
"export const API_PATHS = {",
|
||||
]
|
||||
|
||||
for endpoint in endpoints:
|
||||
# Generate constant name: api_get_users -> GET_USERS
|
||||
const_name = endpoint['id'].replace('api_', '').upper()
|
||||
lines.append(f" {const_name}: '{endpoint['path']}' as const,")
|
||||
|
||||
lines.append("} as const;")
|
||||
lines.append("")
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: generate_api_contract.py <design_document.yml> [--output-dir <dir>]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
design_doc_path = Path(sys.argv[1])
|
||||
|
||||
# Parse output directory
|
||||
output_dir = design_doc_path.parent.parent # .workflow/versions/vXXX/
|
||||
if '--output-dir' in sys.argv:
|
||||
idx = sys.argv.index('--output-dir')
|
||||
output_dir = Path(sys.argv[idx + 1])
|
||||
|
||||
if not design_doc_path.exists():
|
||||
print(f"Error: Design document not found: {design_doc_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Load design document
|
||||
design_doc = load_yaml(design_doc_path)
|
||||
|
||||
if not design_doc:
|
||||
print("Error: Failed to load design document", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Extract entities
|
||||
models = {m['id']: m for m in design_doc.get('data_models', [])}
|
||||
endpoints = design_doc.get('api_endpoints', [])
|
||||
pages = design_doc.get('pages', [])
|
||||
components = design_doc.get('components', [])
|
||||
|
||||
workflow_version = design_doc.get('workflow_version', 'unknown')
|
||||
revision = design_doc.get('revision', 1)
|
||||
|
||||
# Generate types from models
|
||||
types = []
|
||||
for model in models.values():
|
||||
type_def = generate_type_from_model(model)
|
||||
types.append(type_def)
|
||||
|
||||
# Generate request/response types from endpoints
|
||||
endpoints_dict = {e['id']: e for e in endpoints}
|
||||
for endpoint in endpoints:
|
||||
req_type = generate_request_type(endpoint)
|
||||
if req_type:
|
||||
types.append(req_type)
|
||||
|
||||
resp_type = generate_response_type(endpoint, models)
|
||||
if resp_type:
|
||||
types.append(resp_type)
|
||||
|
||||
# Generate types dictionary for lookup
|
||||
types_dict = {t['id']: t for t in types}
|
||||
|
||||
# Generate endpoint contracts
|
||||
endpoint_contracts = []
|
||||
for endpoint in endpoints:
|
||||
contract = generate_endpoint_contract(endpoint, types_dict, models)
|
||||
endpoint_contracts.append(contract)
|
||||
|
||||
# Generate frontend calls
|
||||
frontend_calls = generate_frontend_calls(pages, components, endpoints_dict)
|
||||
|
||||
# Generate backend routes
|
||||
backend_routes = generate_backend_routes(endpoints)
|
||||
|
||||
# Build API contract
|
||||
api_contract = {
|
||||
'api_contract': {
|
||||
'workflow_version': workflow_version,
|
||||
'design_document_revision': revision,
|
||||
'generated_at': datetime.now().isoformat(),
|
||||
'validated_at': None,
|
||||
'status': 'draft',
|
||||
},
|
||||
'types': types,
|
||||
'endpoints': endpoint_contracts,
|
||||
'frontend_calls': frontend_calls,
|
||||
'backend_routes': backend_routes,
|
||||
}
|
||||
|
||||
# Create output directories
|
||||
contracts_dir = output_dir / 'contracts'
|
||||
contracts_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Save API contract
|
||||
contract_path = contracts_dir / 'api_contract.yml'
|
||||
save_yaml(api_contract, contract_path)
|
||||
print(f"Generated: {contract_path}")
|
||||
|
||||
# Generate TypeScript types
|
||||
ts_types = generate_typescript_types(types)
|
||||
ts_paths = generate_api_paths(endpoint_contracts)
|
||||
|
||||
# Find project root (look for package.json)
|
||||
project_root = output_dir
|
||||
while project_root != project_root.parent:
|
||||
if (project_root / 'package.json').exists():
|
||||
break
|
||||
project_root = project_root.parent
|
||||
|
||||
if not (project_root / 'package.json').exists():
|
||||
project_root = output_dir.parent.parent.parent # Assume .workflow is in project root
|
||||
|
||||
# Create types directory and file
|
||||
types_dir = project_root / 'app' / 'types'
|
||||
types_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
types_file = types_dir / 'api.ts'
|
||||
types_file.write_text(ts_types + ts_paths)
|
||||
print(f"Generated: {types_file}")
|
||||
|
||||
# Summary
|
||||
print("\n=== API CONTRACT GENERATED ===")
|
||||
print(f"Types: {len(types)}")
|
||||
print(f"Endpoints: {len(endpoint_contracts)}")
|
||||
print(f"Frontend calls: {len(frontend_calls)}")
|
||||
print(f"Backend routes: {len(backend_routes)}")
|
||||
print(f"\nContract file: {contract_path}")
|
||||
print(f"Types file: {types_file}")
|
||||
print("\nBoth agents MUST import from app/types/api.ts")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Initialize a guardrailed project with manifest."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def create_manifest(name: str, path: str) -> dict:
|
||||
"""Create initial project manifest structure."""
|
||||
return {
|
||||
"project": {
|
||||
"name": name,
|
||||
"version": "0.1.0",
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"description": f"{name} - A guardrailed project"
|
||||
},
|
||||
"state": {
|
||||
"current_phase": "DESIGN_PHASE",
|
||||
"approval_status": {
|
||||
"manifest_approved": False,
|
||||
"approved_by": None,
|
||||
"approved_at": None
|
||||
},
|
||||
"revision_history": [
|
||||
{
|
||||
"action": "PROJECT_INITIALIZED",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"details": f"Project {name} created"
|
||||
}
|
||||
]
|
||||
},
|
||||
"entities": {
|
||||
"pages": [],
|
||||
"components": [],
|
||||
"api_endpoints": [],
|
||||
"database_tables": []
|
||||
},
|
||||
"dependencies": {
|
||||
"component_to_page": {},
|
||||
"api_to_component": {},
|
||||
"table_to_api": {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Initialize guardrailed project")
|
||||
parser.add_argument("--name", required=True, help="Project name")
|
||||
parser.add_argument("--path", required=True, help="Project path")
|
||||
args = parser.parse_args()
|
||||
|
||||
manifest_path = os.path.join(args.path, "project_manifest.json")
|
||||
|
||||
if os.path.exists(manifest_path):
|
||||
print(f"Warning: Manifest already exists at {manifest_path}")
|
||||
print("Use --force to overwrite (not implemented)")
|
||||
return 1
|
||||
|
||||
manifest = create_manifest(args.name, args.path)
|
||||
|
||||
with open(manifest_path, "w") as f:
|
||||
json.dump(manifest, f, indent=2)
|
||||
|
||||
print(f"Initialized guardrailed project: {args.name}")
|
||||
print(f"Manifest created at: {manifest_path}")
|
||||
print(f"Current phase: DESIGN_PHASE")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
|
|
@ -0,0 +1,530 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Manifest diffing and changelog generation between workflow versions.
|
||||
|
||||
Compares project_manifest.json snapshots to show:
|
||||
- Added entities (pages, components, API endpoints)
|
||||
- Removed entities
|
||||
- Modified entities (status changes, path changes)
|
||||
- Dependency changes
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Set, Tuple, Any
|
||||
|
||||
# Try to import yaml
|
||||
try:
|
||||
import yaml
|
||||
HAS_YAML = True
|
||||
except ImportError:
|
||||
HAS_YAML = False
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# File Helpers
|
||||
# ============================================================================
|
||||
|
||||
def load_json(filepath: str) -> dict:
|
||||
"""Load JSON file."""
|
||||
if not os.path.exists(filepath):
|
||||
return {}
|
||||
with open(filepath, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def load_yaml(filepath: str) -> dict:
|
||||
"""Load YAML file."""
|
||||
if not os.path.exists(filepath):
|
||||
return {}
|
||||
with open(filepath, 'r') as f:
|
||||
content = f.read()
|
||||
if not content.strip():
|
||||
return {}
|
||||
if HAS_YAML:
|
||||
return yaml.safe_load(content) or {}
|
||||
return {}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Path Helpers
|
||||
# ============================================================================
|
||||
|
||||
def get_workflow_dir() -> Path:
|
||||
return Path('.workflow')
|
||||
|
||||
|
||||
def get_version_dir(version: str) -> Path:
|
||||
return get_workflow_dir() / 'versions' / version
|
||||
|
||||
|
||||
def get_snapshot_path(version: str, snapshot_type: str) -> Path:
|
||||
"""Get path to manifest snapshot for a version."""
|
||||
return get_version_dir(version) / f'snapshot_{snapshot_type}' / 'manifest.json'
|
||||
|
||||
|
||||
def get_current_manifest_path() -> Path:
|
||||
return Path('project_manifest.json')
|
||||
|
||||
|
||||
def get_versions_list() -> List[str]:
|
||||
"""Get list of all versions."""
|
||||
versions_dir = get_workflow_dir() / 'versions'
|
||||
if not versions_dir.exists():
|
||||
return []
|
||||
return sorted([d.name for d in versions_dir.iterdir() if d.is_dir()])
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Entity Extraction
|
||||
# ============================================================================
|
||||
|
||||
def extract_entities(manifest: dict) -> Dict[str, Dict[str, Any]]:
|
||||
"""
|
||||
Extract all entities from manifest into a flat dict keyed by ID.
|
||||
|
||||
Returns dict like:
|
||||
{
|
||||
"page_home": {"type": "page", "name": "Home", "status": "APPROVED", ...},
|
||||
"component_Button": {"type": "component", "name": "Button", ...},
|
||||
...
|
||||
}
|
||||
"""
|
||||
entities = {}
|
||||
|
||||
entity_types = manifest.get('entities', {})
|
||||
|
||||
for entity_type, entity_list in entity_types.items():
|
||||
if not isinstance(entity_list, list):
|
||||
continue
|
||||
|
||||
for entity in entity_list:
|
||||
entity_id = entity.get('id')
|
||||
if entity_id:
|
||||
entities[entity_id] = {
|
||||
'type': entity_type.rstrip('s'), # pages -> page
|
||||
**entity
|
||||
}
|
||||
|
||||
return entities
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Diff Computation
|
||||
# ============================================================================
|
||||
|
||||
def compute_diff(before: dict, after: dict) -> dict:
|
||||
"""
|
||||
Compute the difference between two manifests.
|
||||
|
||||
Returns:
|
||||
{
|
||||
"added": [list of added entities],
|
||||
"removed": [list of removed entities],
|
||||
"modified": [list of modified entities with changes],
|
||||
"unchanged": [list of unchanged entity IDs]
|
||||
}
|
||||
"""
|
||||
before_entities = extract_entities(before)
|
||||
after_entities = extract_entities(after)
|
||||
|
||||
before_ids = set(before_entities.keys())
|
||||
after_ids = set(after_entities.keys())
|
||||
|
||||
added_ids = after_ids - before_ids
|
||||
removed_ids = before_ids - after_ids
|
||||
common_ids = before_ids & after_ids
|
||||
|
||||
diff = {
|
||||
'added': [],
|
||||
'removed': [],
|
||||
'modified': [],
|
||||
'unchanged': []
|
||||
}
|
||||
|
||||
# Added entities
|
||||
for entity_id in sorted(added_ids):
|
||||
entity = after_entities[entity_id]
|
||||
diff['added'].append({
|
||||
'id': entity_id,
|
||||
'type': entity.get('type'),
|
||||
'name': entity.get('name'),
|
||||
'file_path': entity.get('file_path'),
|
||||
'status': entity.get('status')
|
||||
})
|
||||
|
||||
# Removed entities
|
||||
for entity_id in sorted(removed_ids):
|
||||
entity = before_entities[entity_id]
|
||||
diff['removed'].append({
|
||||
'id': entity_id,
|
||||
'type': entity.get('type'),
|
||||
'name': entity.get('name'),
|
||||
'file_path': entity.get('file_path'),
|
||||
'status': entity.get('status')
|
||||
})
|
||||
|
||||
# Modified entities
|
||||
for entity_id in sorted(common_ids):
|
||||
before_entity = before_entities[entity_id]
|
||||
after_entity = after_entities[entity_id]
|
||||
|
||||
changes = []
|
||||
|
||||
# Check each field for changes
|
||||
for field in ['name', 'file_path', 'status', 'description', 'dependencies']:
|
||||
before_val = before_entity.get(field)
|
||||
after_val = after_entity.get(field)
|
||||
|
||||
if before_val != after_val:
|
||||
changes.append({
|
||||
'field': field,
|
||||
'before': before_val,
|
||||
'after': after_val
|
||||
})
|
||||
|
||||
if changes:
|
||||
diff['modified'].append({
|
||||
'id': entity_id,
|
||||
'type': before_entity.get('type'),
|
||||
'name': after_entity.get('name'),
|
||||
'file_path': after_entity.get('file_path'),
|
||||
'changes': changes
|
||||
})
|
||||
else:
|
||||
diff['unchanged'].append(entity_id)
|
||||
|
||||
return diff
|
||||
|
||||
|
||||
def compute_summary(diff: dict) -> dict:
|
||||
"""Compute summary statistics from diff."""
|
||||
return {
|
||||
'total_added': len(diff['added']),
|
||||
'total_removed': len(diff['removed']),
|
||||
'total_modified': len(diff['modified']),
|
||||
'total_unchanged': len(diff['unchanged']),
|
||||
'by_type': {
|
||||
'pages': {
|
||||
'added': len([e for e in diff['added'] if e['type'] == 'page']),
|
||||
'removed': len([e for e in diff['removed'] if e['type'] == 'page']),
|
||||
'modified': len([e for e in diff['modified'] if e['type'] == 'page'])
|
||||
},
|
||||
'components': {
|
||||
'added': len([e for e in diff['added'] if e['type'] == 'component']),
|
||||
'removed': len([e for e in diff['removed'] if e['type'] == 'component']),
|
||||
'modified': len([e for e in diff['modified'] if e['type'] == 'component'])
|
||||
},
|
||||
'api_endpoints': {
|
||||
'added': len([e for e in diff['added'] if e['type'] == 'api_endpoint']),
|
||||
'removed': len([e for e in diff['removed'] if e['type'] == 'api_endpoint']),
|
||||
'modified': len([e for e in diff['modified'] if e['type'] == 'api_endpoint'])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Display Functions
|
||||
# ============================================================================
|
||||
|
||||
def format_entity(entity: dict, prefix: str = '') -> str:
|
||||
"""Format an entity for display."""
|
||||
type_icon = {
|
||||
'page': '📄',
|
||||
'component': '🧩',
|
||||
'api_endpoint': '🔌',
|
||||
'lib': '📚',
|
||||
'hook': '🪝',
|
||||
'type': '📝',
|
||||
'config': '⚙️'
|
||||
}.get(entity.get('type', ''), '•')
|
||||
|
||||
name = entity.get('name', entity.get('id', 'Unknown'))
|
||||
file_path = entity.get('file_path', '')
|
||||
|
||||
return f"{prefix}{type_icon} {name} ({file_path})"
|
||||
|
||||
|
||||
def display_diff(diff: dict, summary: dict, from_version: str, to_version: str):
|
||||
"""Display diff in a formatted way."""
|
||||
print()
|
||||
print("╔" + "═" * 70 + "╗")
|
||||
print("║" + f" MANIFEST DIFF: {from_version} → {to_version}".ljust(70) + "║")
|
||||
print("╠" + "═" * 70 + "╣")
|
||||
|
||||
# Summary
|
||||
print("║" + " SUMMARY".ljust(70) + "║")
|
||||
print("║" + f" + Added: {summary['total_added']}".ljust(70) + "║")
|
||||
print("║" + f" ~ Modified: {summary['total_modified']}".ljust(70) + "║")
|
||||
print("║" + f" - Removed: {summary['total_removed']}".ljust(70) + "║")
|
||||
print("║" + f" = Unchanged: {summary['total_unchanged']}".ljust(70) + "║")
|
||||
|
||||
# By type
|
||||
print("╠" + "═" * 70 + "╣")
|
||||
print("║" + " BY TYPE".ljust(70) + "║")
|
||||
for type_name, counts in summary['by_type'].items():
|
||||
changes = []
|
||||
if counts['added'] > 0:
|
||||
changes.append(f"+{counts['added']}")
|
||||
if counts['modified'] > 0:
|
||||
changes.append(f"~{counts['modified']}")
|
||||
if counts['removed'] > 0:
|
||||
changes.append(f"-{counts['removed']}")
|
||||
if changes:
|
||||
print("║" + f" {type_name}: {' '.join(changes)}".ljust(70) + "║")
|
||||
|
||||
# Added
|
||||
if diff['added']:
|
||||
print("╠" + "═" * 70 + "╣")
|
||||
print("║" + " ➕ ADDED".ljust(70) + "║")
|
||||
for entity in diff['added']:
|
||||
line = format_entity(entity, ' + ')
|
||||
print("║" + line[:70].ljust(70) + "║")
|
||||
|
||||
# Modified
|
||||
if diff['modified']:
|
||||
print("╠" + "═" * 70 + "╣")
|
||||
print("║" + " 📝 MODIFIED".ljust(70) + "║")
|
||||
for entity in diff['modified']:
|
||||
line = format_entity(entity, ' ~ ')
|
||||
print("║" + line[:70].ljust(70) + "║")
|
||||
for change in entity['changes']:
|
||||
field = change['field']
|
||||
before = str(change['before'])[:20] if change['before'] else '(none)'
|
||||
after = str(change['after'])[:20] if change['after'] else '(none)'
|
||||
change_line = f" {field}: {before} → {after}"
|
||||
print("║" + change_line[:70].ljust(70) + "║")
|
||||
|
||||
# Removed
|
||||
if diff['removed']:
|
||||
print("╠" + "═" * 70 + "╣")
|
||||
print("║" + " ➖ REMOVED".ljust(70) + "║")
|
||||
for entity in diff['removed']:
|
||||
line = format_entity(entity, ' - ')
|
||||
print("║" + line[:70].ljust(70) + "║")
|
||||
|
||||
print("╚" + "═" * 70 + "╝")
|
||||
|
||||
|
||||
def display_changelog(version: str, session: dict, diff: dict, summary: dict):
|
||||
"""Display changelog for a single version."""
|
||||
print()
|
||||
print("╔" + "═" * 70 + "╗")
|
||||
print("║" + f" CHANGELOG: {version}".ljust(70) + "║")
|
||||
print("╠" + "═" * 70 + "╣")
|
||||
print("║" + f" Feature: {session.get('feature', 'Unknown')[:55]}".ljust(70) + "║")
|
||||
print("║" + f" Status: {session.get('status', 'unknown')}".ljust(70) + "║")
|
||||
|
||||
if session.get('started_at'):
|
||||
print("║" + f" Started: {session['started_at'][:19]}".ljust(70) + "║")
|
||||
|
||||
if session.get('completed_at'):
|
||||
print("║" + f" Completed: {session['completed_at'][:19]}".ljust(70) + "║")
|
||||
|
||||
print("╠" + "═" * 70 + "╣")
|
||||
print("║" + " CHANGES".ljust(70) + "║")
|
||||
|
||||
if not diff['added'] and not diff['modified'] and not diff['removed']:
|
||||
print("║" + " No entity changes".ljust(70) + "║")
|
||||
else:
|
||||
for entity in diff['added']:
|
||||
line = f" + Added {entity['type']}: {entity['name']}"
|
||||
print("║" + line[:70].ljust(70) + "║")
|
||||
|
||||
for entity in diff['modified']:
|
||||
line = f" ~ Modified {entity['type']}: {entity['name']}"
|
||||
print("║" + line[:70].ljust(70) + "║")
|
||||
|
||||
for entity in diff['removed']:
|
||||
line = f" - Removed {entity['type']}: {entity['name']}"
|
||||
print("║" + line[:70].ljust(70) + "║")
|
||||
|
||||
print("╚" + "═" * 70 + "╝")
|
||||
|
||||
|
||||
def output_json(data: dict):
|
||||
"""Output data as JSON."""
|
||||
print(json.dumps(data, indent=2))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Commands
|
||||
# ============================================================================
|
||||
|
||||
def diff_versions(version1: str, version2: str, output_format: str = 'text') -> int:
|
||||
"""Diff two specific versions."""
|
||||
# Load snapshots
|
||||
before_path = get_snapshot_path(version1, 'after')
|
||||
if not before_path.exists():
|
||||
before_path = get_snapshot_path(version1, 'before')
|
||||
|
||||
after_path = get_snapshot_path(version2, 'after')
|
||||
if not after_path.exists():
|
||||
after_path = get_snapshot_path(version2, 'before')
|
||||
|
||||
if not before_path.exists():
|
||||
print(f"Error: No snapshot found for version {version1}")
|
||||
return 1
|
||||
|
||||
if not after_path.exists():
|
||||
print(f"Error: No snapshot found for version {version2}")
|
||||
return 1
|
||||
|
||||
before = load_json(str(before_path))
|
||||
after = load_json(str(after_path))
|
||||
|
||||
diff = compute_diff(before, after)
|
||||
summary = compute_summary(diff)
|
||||
|
||||
if output_format == 'json':
|
||||
output_json({
|
||||
'from_version': version1,
|
||||
'to_version': version2,
|
||||
'diff': diff,
|
||||
'summary': summary
|
||||
})
|
||||
else:
|
||||
display_diff(diff, summary, version1, version2)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def diff_with_current(version: str, output_format: str = 'text') -> int:
|
||||
"""Diff a version with current manifest."""
|
||||
# Load version snapshot
|
||||
snapshot_path = get_snapshot_path(version, 'before')
|
||||
if not snapshot_path.exists():
|
||||
print(f"Error: No snapshot found for version {version}")
|
||||
return 1
|
||||
|
||||
before = load_json(str(snapshot_path))
|
||||
|
||||
# Load current manifest
|
||||
current_path = get_current_manifest_path()
|
||||
if not current_path.exists():
|
||||
print("Error: No current manifest found")
|
||||
return 1
|
||||
|
||||
after = load_json(str(current_path))
|
||||
|
||||
diff = compute_diff(before, after)
|
||||
summary = compute_summary(diff)
|
||||
|
||||
if output_format == 'json':
|
||||
output_json({
|
||||
'from_version': version,
|
||||
'to_version': 'current',
|
||||
'diff': diff,
|
||||
'summary': summary
|
||||
})
|
||||
else:
|
||||
display_diff(diff, summary, version, 'current')
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def show_changelog(version: str = None, output_format: str = 'text') -> int:
|
||||
"""Show changelog for a version or all versions."""
|
||||
versions = get_versions_list()
|
||||
|
||||
if not versions:
|
||||
print("No workflow versions found.")
|
||||
return 1
|
||||
|
||||
if version:
|
||||
versions = [v for v in versions if v == version]
|
||||
if not versions:
|
||||
print(f"Version {version} not found.")
|
||||
return 1
|
||||
|
||||
for i, v in enumerate(versions):
|
||||
# Load session info
|
||||
session_path = get_version_dir(v) / 'session.yml'
|
||||
session = load_yaml(str(session_path)) if session_path.exists() else {}
|
||||
|
||||
# Get before/after snapshots
|
||||
before_path = get_snapshot_path(v, 'before')
|
||||
after_path = get_snapshot_path(v, 'after')
|
||||
|
||||
before = load_json(str(before_path)) if before_path.exists() else {}
|
||||
after = load_json(str(after_path)) if after_path.exists() else {}
|
||||
|
||||
if not after:
|
||||
after = before # Use before if no after exists
|
||||
|
||||
diff = compute_diff(before, after)
|
||||
summary = compute_summary(diff)
|
||||
|
||||
if output_format == 'json':
|
||||
output_json({
|
||||
'version': v,
|
||||
'session': session,
|
||||
'diff': diff,
|
||||
'summary': summary
|
||||
})
|
||||
else:
|
||||
display_changelog(v, session, diff, summary)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CLI Interface
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Manifest diffing and changelog generation")
|
||||
subparsers = parser.add_subparsers(dest='command', help='Commands')
|
||||
|
||||
# diff command
|
||||
diff_parser = subparsers.add_parser('diff', help='Diff two versions')
|
||||
diff_parser.add_argument('version1', help='First version')
|
||||
diff_parser.add_argument('version2', nargs='?', help='Second version (or "current")')
|
||||
diff_parser.add_argument('--json', action='store_true', help='Output as JSON')
|
||||
|
||||
# changelog command
|
||||
changelog_parser = subparsers.add_parser('changelog', help='Show version changelog')
|
||||
changelog_parser.add_argument('version', nargs='?', help='Specific version (or all)')
|
||||
changelog_parser.add_argument('--json', action='store_true', help='Output as JSON')
|
||||
|
||||
# versions command
|
||||
subparsers.add_parser('versions', help='List all versions')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == 'diff':
|
||||
output_format = 'json' if args.json else 'text'
|
||||
|
||||
if args.version2:
|
||||
if args.version2 == 'current':
|
||||
sys.exit(diff_with_current(args.version1, output_format))
|
||||
else:
|
||||
sys.exit(diff_versions(args.version1, args.version2, output_format))
|
||||
else:
|
||||
# Diff with current by default
|
||||
sys.exit(diff_with_current(args.version1, output_format))
|
||||
|
||||
elif args.command == 'changelog':
|
||||
output_format = 'json' if args.json else 'text'
|
||||
sys.exit(show_changelog(args.version, output_format))
|
||||
|
||||
elif args.command == 'versions':
|
||||
versions = get_versions_list()
|
||||
if versions:
|
||||
print("\nAvailable versions:")
|
||||
for v in versions:
|
||||
print(f" - {v}")
|
||||
else:
|
||||
print("No versions found.")
|
||||
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,265 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script to convert flat task session files to directory structure.
|
||||
|
||||
This script migrates task sessions from the old flat file structure:
|
||||
.workflow/versions/v001/task_sessions/task_design.yml
|
||||
|
||||
To the new directory structure:
|
||||
.workflow/versions/v001/task_sessions/task_design/
|
||||
session.yml
|
||||
task.yml
|
||||
operations.log
|
||||
|
||||
Usage:
|
||||
python3 migrate_task_sessions.py [--dry-run]
|
||||
|
||||
Options:
|
||||
--dry-run Show what would be done without making changes
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
# Add parent to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from version_manager import load_yaml, save_yaml, get_workflow_dir
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Discovery Functions
|
||||
# ============================================================================
|
||||
|
||||
def find_flat_task_sessions() -> list[tuple[Path, str]]:
|
||||
"""
|
||||
Find all flat task session YAML files.
|
||||
|
||||
Returns:
|
||||
List of tuples: (file_path, version_name)
|
||||
"""
|
||||
workflow_dir = get_workflow_dir()
|
||||
versions_dir = workflow_dir / 'versions'
|
||||
|
||||
flat_sessions = []
|
||||
if versions_dir.exists():
|
||||
for version_dir in versions_dir.iterdir():
|
||||
if version_dir.is_dir():
|
||||
task_sessions_dir = version_dir / 'task_sessions'
|
||||
if task_sessions_dir.exists():
|
||||
for item in task_sessions_dir.iterdir():
|
||||
# Check if it's a YAML file (not a directory)
|
||||
if item.is_file() and item.suffix in ['.yml', '.yaml']:
|
||||
flat_sessions.append((item, version_dir.name))
|
||||
|
||||
return flat_sessions
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Migration Functions
|
||||
# ============================================================================
|
||||
|
||||
def migrate_task_session(file_path: Path, version: str, dry_run: bool = False) -> dict:
|
||||
"""
|
||||
Migrate a single flat task session to directory structure.
|
||||
|
||||
Args:
|
||||
file_path: Path to the flat YAML file
|
||||
version: Version identifier (e.g., 'v001')
|
||||
dry_run: If True, only report what would be done
|
||||
|
||||
Returns:
|
||||
Dictionary with migration results and actions taken
|
||||
"""
|
||||
task_id = file_path.stem # e.g., "task_design" from "task_design.yml"
|
||||
parent_dir = file_path.parent
|
||||
new_dir = parent_dir / task_id
|
||||
|
||||
result = {
|
||||
'task_id': task_id,
|
||||
'version': version,
|
||||
'original_path': str(file_path),
|
||||
'new_path': str(new_dir),
|
||||
'success': False,
|
||||
'actions': []
|
||||
}
|
||||
|
||||
if dry_run:
|
||||
result['actions'].append(f"Would create directory: {new_dir}")
|
||||
result['actions'].append(f"Would move {file_path.name} to {new_dir}/session.yml")
|
||||
result['actions'].append(f"Would create {new_dir}/task.yml (if source exists)")
|
||||
result['actions'].append(f"Would create {new_dir}/operations.log")
|
||||
result['success'] = True
|
||||
return result
|
||||
|
||||
try:
|
||||
# Create directory
|
||||
new_dir.mkdir(exist_ok=True)
|
||||
result['actions'].append(f"Created directory: {new_dir}")
|
||||
|
||||
# Move session file
|
||||
session_data = load_yaml(str(file_path))
|
||||
save_yaml(str(new_dir / 'session.yml'), session_data)
|
||||
file_path.unlink() # Delete original
|
||||
result['actions'].append(f"Moved session data to: {new_dir}/session.yml")
|
||||
|
||||
# Create task.yml snapshot (try to find original task)
|
||||
task_file = Path('tasks') / f'{task_id}.yml'
|
||||
if task_file.exists():
|
||||
task_data = load_yaml(str(task_file))
|
||||
task_data['snapshotted_at'] = datetime.now().isoformat()
|
||||
task_data['source_path'] = str(task_file)
|
||||
task_data['status_at_snapshot'] = task_data.get('status', 'migrated')
|
||||
task_data['migration_note'] = 'Created during migration from flat file structure'
|
||||
save_yaml(str(new_dir / 'task.yml'), task_data)
|
||||
result['actions'].append(f"Created task snapshot: {new_dir}/task.yml")
|
||||
else:
|
||||
# Create minimal task.yml from session data
|
||||
minimal_task = {
|
||||
'id': task_id,
|
||||
'type': session_data.get('task_type', 'unknown'),
|
||||
'agent': session_data.get('agent', 'unknown'),
|
||||
'snapshotted_at': datetime.now().isoformat(),
|
||||
'source_path': 'N/A - reconstructed from session',
|
||||
'status_at_snapshot': 'migrated',
|
||||
'migration_note': 'Task file not found - reconstructed from session data'
|
||||
}
|
||||
save_yaml(str(new_dir / 'task.yml'), minimal_task)
|
||||
result['actions'].append(f"Warning: Task file not found at {task_file}")
|
||||
result['actions'].append(f"Created minimal task snapshot: {new_dir}/task.yml")
|
||||
|
||||
# Create operations.log
|
||||
log_content = f"# Operations Log for {task_id}\n"
|
||||
log_content += f"# Migrated: {datetime.now().isoformat()}\n"
|
||||
log_content += "# Format: [timestamp] OPERATION target_type: target_id (path)\n"
|
||||
log_content += "=" * 70 + "\n\n"
|
||||
log_content += f"[{datetime.now().isoformat()}] MIGRATION: Converted from flat file structure\n"
|
||||
|
||||
# If session has operations, add them to the log
|
||||
if 'operations' in session_data and session_data['operations']:
|
||||
log_content += f"\n# Historical operations from session data:\n"
|
||||
for op in session_data['operations']:
|
||||
timestamp = op.get('performed_at', 'unknown')
|
||||
op_type = op.get('type', 'UNKNOWN')
|
||||
target_type = op.get('target_type', 'unknown')
|
||||
target_id = op.get('target_id', 'unknown')
|
||||
target_path = op.get('target_path', '')
|
||||
|
||||
entry = f"[{timestamp}] {op_type} {target_type}: {target_id}"
|
||||
if target_path:
|
||||
entry += f" ({target_path})"
|
||||
|
||||
diff_summary = op.get('changes', {}).get('diff_summary', '')
|
||||
if diff_summary:
|
||||
entry += f"\n Summary: {diff_summary}"
|
||||
|
||||
log_content += entry + "\n"
|
||||
|
||||
(new_dir / 'operations.log').write_text(log_content)
|
||||
result['actions'].append(f"Created operations log: {new_dir}/operations.log")
|
||||
|
||||
result['success'] = True
|
||||
|
||||
except Exception as e:
|
||||
result['error'] = str(e)
|
||||
result['actions'].append(f"Error: {e}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main Entry Point
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
"""Main migration script entry point."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Migrate task session files from flat structure to directories',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help='Show what would be done without making changes'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
dry_run = args.dry_run
|
||||
|
||||
# Header
|
||||
print("=" * 70)
|
||||
print("Task Session Migration Script".center(70))
|
||||
print(f"Mode: {'DRY RUN' if dry_run else 'LIVE MIGRATION'}".center(70))
|
||||
print("=" * 70)
|
||||
print()
|
||||
|
||||
# Find flat sessions
|
||||
flat_sessions = find_flat_task_sessions()
|
||||
|
||||
if not flat_sessions:
|
||||
print("No flat task session files found. Nothing to migrate.")
|
||||
print()
|
||||
print("This could mean:")
|
||||
print(" 1. All task sessions are already migrated")
|
||||
print(" 2. No task sessions exist yet")
|
||||
print(" 3. .workflow directory doesn't exist")
|
||||
return
|
||||
|
||||
print(f"Found {len(flat_sessions)} flat task session file(s) to migrate:")
|
||||
print()
|
||||
|
||||
# Process each file
|
||||
results = []
|
||||
for file_path, version in flat_sessions:
|
||||
print(f"Processing: {version}/{file_path.name}")
|
||||
print("-" * 70)
|
||||
|
||||
result = migrate_task_session(file_path, version, dry_run)
|
||||
results.append(result)
|
||||
|
||||
for action in result['actions']:
|
||||
print(f" {action}")
|
||||
|
||||
if not result['success'] and 'error' in result:
|
||||
print(f" ERROR: {result['error']}")
|
||||
|
||||
print()
|
||||
|
||||
# Summary
|
||||
successful = sum(1 for r in results if r['success'])
|
||||
failed = len(results) - successful
|
||||
|
||||
print("=" * 70)
|
||||
print("Migration Summary".center(70))
|
||||
print("=" * 70)
|
||||
print(f"Total files processed: {len(results)}")
|
||||
print(f"Successful migrations: {successful}")
|
||||
print(f"Failed migrations: {failed}")
|
||||
print()
|
||||
|
||||
if dry_run:
|
||||
print("This was a DRY RUN. No files were modified.")
|
||||
print("Run without --dry-run to perform the migration.")
|
||||
else:
|
||||
if successful > 0:
|
||||
print("Migration completed successfully!")
|
||||
print()
|
||||
print("Next steps:")
|
||||
print(" 1. Verify migrated files in .workflow/versions/*/task_sessions/")
|
||||
print(" 2. Check that each task has session.yml, task.yml, and operations.log")
|
||||
print(" 3. Test the system to ensure compatibility")
|
||||
|
||||
if failed > 0:
|
||||
print()
|
||||
print(f"WARNING: {failed} migration(s) failed. Review the errors above.")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Post-write hook to update entity status in manifest."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def load_manifest(manifest_path: str) -> dict | None:
|
||||
"""Load manifest if it exists."""
|
||||
if not os.path.exists(manifest_path):
|
||||
return None
|
||||
with open(manifest_path) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def save_manifest(manifest_path: str, manifest: dict):
|
||||
"""Save manifest to file."""
|
||||
with open(manifest_path, "w") as f:
|
||||
json.dump(manifest, f, indent=2)
|
||||
|
||||
|
||||
def find_entity_by_path(manifest: dict, file_path: str) -> tuple:
|
||||
"""Find entity by file path, return (entity_type, index, entity)."""
|
||||
entities = manifest.get("entities", {})
|
||||
for entity_type in ["pages", "components", "api_endpoints", "database_tables"]:
|
||||
for idx, entity in enumerate(entities.get(entity_type, [])):
|
||||
if entity.get("file_path") == file_path:
|
||||
return (entity_type, idx, entity)
|
||||
return (None, None, None)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Post-write hook")
|
||||
parser.add_argument("--manifest", required=True, help="Path to manifest")
|
||||
parser.add_argument("--file", help="File that was written")
|
||||
args = parser.parse_args()
|
||||
|
||||
manifest = load_manifest(args.manifest)
|
||||
|
||||
if manifest is None:
|
||||
return 0
|
||||
|
||||
# If file provided, update entity status
|
||||
if args.file:
|
||||
# Normalize the file path
|
||||
file_path = args.file.lstrip('./')
|
||||
|
||||
entity_type, idx, entity = find_entity_by_path(manifest, args.file)
|
||||
|
||||
# Try without leading ./
|
||||
if not entity:
|
||||
entity_type, idx, entity = find_entity_by_path(manifest, file_path)
|
||||
|
||||
if entity and entity.get("status") == "APPROVED":
|
||||
manifest["entities"][entity_type][idx]["status"] = "IMPLEMENTED"
|
||||
manifest["entities"][entity_type][idx]["implemented_at"] = datetime.now().isoformat()
|
||||
|
||||
# Add to history (ensure it exists)
|
||||
if "state" not in manifest:
|
||||
manifest["state"] = {}
|
||||
if "revision_history" not in manifest["state"]:
|
||||
manifest["state"]["revision_history"] = []
|
||||
|
||||
manifest["state"]["revision_history"].append({
|
||||
"action": "ENTITY_IMPLEMENTED",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"details": f"Implemented {entity.get('id', 'unknown')}"
|
||||
})
|
||||
|
||||
save_manifest(args.manifest, manifest)
|
||||
print(f"GUARDRAIL: Updated {entity.get('id')} to IMPLEMENTED")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
|
|
@ -0,0 +1,601 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Comprehensive Security Scanner for guardrail workflow.
|
||||
|
||||
Performs static security analysis on codebase:
|
||||
- Hardcoded secrets and credentials
|
||||
- SQL injection vulnerabilities
|
||||
- XSS vulnerabilities
|
||||
- Path traversal risks
|
||||
- Insecure dependencies
|
||||
- Authentication/Authorization issues
|
||||
- OWASP Top 10 patterns
|
||||
|
||||
Usage:
|
||||
python3 security_scan.py --project-dir . [--severity critical|high|medium|low]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import NamedTuple
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class SecurityIssue:
|
||||
"""Security vulnerability finding."""
|
||||
severity: str # CRITICAL, HIGH, MEDIUM, LOW, INFO
|
||||
category: str
|
||||
title: str
|
||||
description: str
|
||||
file_path: str
|
||||
line_number: int | None
|
||||
code_snippet: str
|
||||
recommendation: str
|
||||
cwe_id: str | None = None
|
||||
owasp_category: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScanResult:
|
||||
"""Complete scan results."""
|
||||
issues: list[SecurityIssue] = field(default_factory=list)
|
||||
files_scanned: int = 0
|
||||
scan_duration: float = 0.0
|
||||
|
||||
|
||||
# Security patterns organized by category
|
||||
SECURITY_PATTERNS = {
|
||||
'hardcoded_secrets': {
|
||||
'severity': 'CRITICAL',
|
||||
'cwe': 'CWE-798',
|
||||
'owasp': 'A07:2021-Identification and Authentication Failures',
|
||||
'patterns': [
|
||||
# API Keys
|
||||
(r'''(?:api[_-]?key|apikey)\s*[:=]\s*['"]((?!process\.env)[^'"]{10,})['"']''', 'Hardcoded API key'),
|
||||
(r'''(?:api[_-]?secret|apisecret)\s*[:=]\s*['"]((?!process\.env)[^'"]{10,})['"']''', 'Hardcoded API secret'),
|
||||
# Passwords
|
||||
(r'''(?:password|passwd|pwd)\s*[:=]\s*['"]([^'"]{4,})['"']''', 'Hardcoded password'),
|
||||
# Private keys
|
||||
(r'''-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----''', 'Embedded private key'),
|
||||
# AWS credentials
|
||||
(r'''(?:aws[_-]?access[_-]?key[_-]?id|aws[_-]?secret)\s*[:=]\s*['"]([A-Z0-9]{16,})['"']''', 'AWS credential'),
|
||||
(r'''AKIA[0-9A-Z]{16}''', 'AWS Access Key ID'),
|
||||
# JWT secrets
|
||||
(r'''(?:jwt[_-]?secret|token[_-]?secret)\s*[:=]\s*['"]([^'"]{8,})['"']''', 'Hardcoded JWT secret'),
|
||||
# Database connection strings
|
||||
(r'''(?:mongodb|postgres|mysql|redis):\/\/[^:]+:[^@]+@''', 'Database credentials in connection string'),
|
||||
# Generic secrets
|
||||
(r'''(?:secret|token|auth)[_-]?(?:key)?\s*[:=]\s*['"]([^'"]{8,})['"']''', 'Potential hardcoded secret'),
|
||||
]
|
||||
},
|
||||
'sql_injection': {
|
||||
'severity': 'CRITICAL',
|
||||
'cwe': 'CWE-89',
|
||||
'owasp': 'A03:2021-Injection',
|
||||
'patterns': [
|
||||
# String concatenation in queries
|
||||
(r'''(?:query|sql|execute)\s*\(\s*[`'"].*\$\{''', 'SQL injection via template literal'),
|
||||
(r'''(?:query|sql|execute)\s*\(\s*['"].*\+\s*(?:req\.|params\.|body\.|query\.)''', 'SQL injection via concatenation'),
|
||||
(r'''(?:SELECT|INSERT|UPDATE|DELETE|FROM|WHERE).*\$\{''', 'Raw SQL with template interpolation'),
|
||||
# Raw queries
|
||||
(r'''\.raw\s*\(\s*[`'"].*\$\{''', 'Raw query with interpolation'),
|
||||
(r'''prisma\.\$queryRaw\s*`[^`]*\$\{''', 'Prisma raw query with interpolation'),
|
||||
]
|
||||
},
|
||||
'xss': {
|
||||
'severity': 'HIGH',
|
||||
'cwe': 'CWE-79',
|
||||
'owasp': 'A03:2021-Injection',
|
||||
'patterns': [
|
||||
# React dangerouslySetInnerHTML
|
||||
(r'''dangerouslySetInnerHTML\s*=\s*\{\s*\{__html:\s*(?!DOMPurify|sanitize)''', 'Unsanitized dangerouslySetInnerHTML'),
|
||||
# innerHTML assignment
|
||||
(r'''\.innerHTML\s*=\s*(?!['"`]<)''', 'Direct innerHTML assignment'),
|
||||
# document.write
|
||||
(r'''document\.write\s*\(''', 'document.write usage'),
|
||||
# eval with user input
|
||||
(r'''eval\s*\(\s*(?:req\.|params\.|body\.|query\.|props\.)''', 'eval with user input'),
|
||||
# jQuery html() with user input
|
||||
(r'''\$\([^)]+\)\.html\s*\(\s*(?!['"`])''', 'jQuery html() with dynamic content'),
|
||||
]
|
||||
},
|
||||
'path_traversal': {
|
||||
'severity': 'HIGH',
|
||||
'cwe': 'CWE-22',
|
||||
'owasp': 'A01:2021-Broken Access Control',
|
||||
'patterns': [
|
||||
# File operations with user input
|
||||
(r'''(?:readFile|writeFile|readFileSync|writeFileSync|createReadStream)\s*\(\s*(?:req\.|params\.|body\.|query\.)''', 'File operation with user input'),
|
||||
(r'''(?:readFile|writeFile)\s*\(\s*[`'"].*\$\{(?:req\.|params\.|body\.|query\.)''', 'File path with user input interpolation'),
|
||||
# Path.join with user input (without validation)
|
||||
(r'''path\.(?:join|resolve)\s*\([^)]*(?:req\.|params\.|body\.|query\.)''', 'Path operation with user input'),
|
||||
]
|
||||
},
|
||||
'command_injection': {
|
||||
'severity': 'CRITICAL',
|
||||
'cwe': 'CWE-78',
|
||||
'owasp': 'A03:2021-Injection',
|
||||
'patterns': [
|
||||
# exec/spawn with user input
|
||||
(r'''(?:exec|execSync|spawn|spawnSync)\s*\(\s*[`'"].*\$\{''', 'Command injection via template literal'),
|
||||
(r'''(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:req\.|params\.|body\.|query\.)''', 'Command execution with user input'),
|
||||
# child_process with concatenation
|
||||
(r'''child_process.*\(\s*['"].*\+\s*(?:req\.|params\.|body\.|query\.)''', 'Command injection via concatenation'),
|
||||
]
|
||||
},
|
||||
'insecure_auth': {
|
||||
'severity': 'HIGH',
|
||||
'cwe': 'CWE-287',
|
||||
'owasp': 'A07:2021-Identification and Authentication Failures',
|
||||
'patterns': [
|
||||
# Weak JWT algorithms
|
||||
(r'''algorithm\s*[:=]\s*['"](?:none|HS256)['"']''', 'Weak JWT algorithm'),
|
||||
# No password hashing
|
||||
(r'''password\s*===?\s*(?:req\.|body\.|params\.)''', 'Plain text password comparison'),
|
||||
# Disabled security
|
||||
(r'''(?:verify|secure|https|ssl)\s*[:=]\s*false''', 'Security feature disabled'),
|
||||
# Cookie without security flags
|
||||
(r'''cookie\s*\([^)]*\)\s*(?!.*(?:httpOnly|secure|sameSite))''', 'Cookie without security flags'),
|
||||
]
|
||||
},
|
||||
'sensitive_data_exposure': {
|
||||
'severity': 'MEDIUM',
|
||||
'cwe': 'CWE-200',
|
||||
'owasp': 'A02:2021-Cryptographic Failures',
|
||||
'patterns': [
|
||||
# Logging sensitive data
|
||||
(r'''console\.(?:log|info|debug)\s*\([^)]*(?:password|secret|token|key|credential)''', 'Logging sensitive data'),
|
||||
# Error messages with sensitive info
|
||||
(r'''(?:throw|Error)\s*\([^)]*(?:password|secret|token|key|sql|query)''', 'Sensitive info in error message'),
|
||||
# HTTP instead of HTTPS
|
||||
(r'''['"]http:\/\/(?!localhost|127\.0\.0\.1)''', 'HTTP URL (should be HTTPS)'),
|
||||
]
|
||||
},
|
||||
'insecure_dependencies': {
|
||||
'severity': 'MEDIUM',
|
||||
'cwe': 'CWE-1104',
|
||||
'owasp': 'A06:2021-Vulnerable and Outdated Components',
|
||||
'patterns': [
|
||||
# Known vulnerable patterns
|
||||
(r'''require\s*\(\s*['"](?:serialize-javascript|lodash\.template|node-serialize)['"]\s*\)''', 'Known vulnerable package'),
|
||||
# Outdated crypto
|
||||
(r'''crypto\.createCipher\s*\(''', 'Deprecated crypto.createCipher'),
|
||||
(r'''md5\s*\(|createHash\s*\(\s*['"]md5['"]''', 'MD5 hash usage (weak)'),
|
||||
(r'''sha1\s*\(|createHash\s*\(\s*['"]sha1['"]''', 'SHA1 hash usage (weak)'),
|
||||
]
|
||||
},
|
||||
'cors_misconfiguration': {
|
||||
'severity': 'MEDIUM',
|
||||
'cwe': 'CWE-942',
|
||||
'owasp': 'A01:2021-Broken Access Control',
|
||||
'patterns': [
|
||||
# Wildcard CORS
|
||||
(r'''(?:Access-Control-Allow-Origin|origin)\s*[:=]\s*['"]\*['"']''', 'Wildcard CORS origin'),
|
||||
(r'''cors\s*\(\s*\{[^}]*origin\s*:\s*true''', 'CORS allows all origins'),
|
||||
# Credentials with wildcard
|
||||
(r'''credentials\s*:\s*true[^}]*origin\s*:\s*['"]\*['"']''', 'CORS credentials with wildcard origin'),
|
||||
]
|
||||
},
|
||||
'insecure_randomness': {
|
||||
'severity': 'LOW',
|
||||
'cwe': 'CWE-330',
|
||||
'owasp': 'A02:2021-Cryptographic Failures',
|
||||
'patterns': [
|
||||
# Math.random for security
|
||||
(r'''Math\.random\s*\(\s*\)[^;]*(?:token|secret|password|key|id|session)''', 'Math.random for security-sensitive value'),
|
||||
(r'''(?:token|secret|key|session)[^=]*=\s*Math\.random''', 'Math.random for security-sensitive value'),
|
||||
]
|
||||
},
|
||||
'debug_code': {
|
||||
'severity': 'LOW',
|
||||
'cwe': 'CWE-489',
|
||||
'owasp': 'A05:2021-Security Misconfiguration',
|
||||
'patterns': [
|
||||
# Debug statements
|
||||
(r'''console\.(?:log|debug|info|warn)\s*\(''', 'Console statement (remove in production)'),
|
||||
(r'''debugger\s*;''', 'Debugger statement'),
|
||||
# TODO/FIXME security notes
|
||||
(r'''(?:TODO|FIXME|HACK|XXX).*(?:security|auth|password|secret|vulnerable)''', 'Security-related TODO'),
|
||||
]
|
||||
},
|
||||
'nosql_injection': {
|
||||
'severity': 'HIGH',
|
||||
'cwe': 'CWE-943',
|
||||
'owasp': 'A03:2021-Injection',
|
||||
'patterns': [
|
||||
# MongoDB injection
|
||||
(r'''\.find\s*\(\s*\{[^}]*\$(?:where|regex|gt|lt|ne|in|nin|or|and).*(?:req\.|params\.|body\.|query\.)''', 'NoSQL injection risk'),
|
||||
(r'''\.find\s*\(\s*(?:req\.|params\.|body\.|query\.)''', 'Direct user input in query'),
|
||||
]
|
||||
},
|
||||
'prototype_pollution': {
|
||||
'severity': 'HIGH',
|
||||
'cwe': 'CWE-1321',
|
||||
'owasp': 'A03:2021-Injection',
|
||||
'patterns': [
|
||||
# Deep merge without protection
|
||||
(r'''(?:merge|extend|assign)\s*\([^)]*(?:req\.|params\.|body\.|query\.)''', 'Potential prototype pollution via merge'),
|
||||
(r'''Object\.assign\s*\(\s*\{\}[^)]*(?:req\.|params\.|body\.|query\.)''', 'Object.assign with user input'),
|
||||
# __proto__ access
|
||||
(r'''__proto__''', 'Direct __proto__ access'),
|
||||
(r'''constructor\s*\[\s*['"]prototype['"]''', 'Prototype access via constructor'),
|
||||
]
|
||||
},
|
||||
'ssrf': {
|
||||
'severity': 'HIGH',
|
||||
'cwe': 'CWE-918',
|
||||
'owasp': 'A10:2021-Server-Side Request Forgery',
|
||||
'patterns': [
|
||||
# Fetch/axios with user URL
|
||||
(r'''(?:fetch|axios\.get|axios\.post|http\.get|https\.get)\s*\(\s*(?:req\.|params\.|body\.|query\.)''', 'SSRF via user-controlled URL'),
|
||||
(r'''(?:fetch|axios)\s*\(\s*[`'"].*\$\{(?:req\.|params\.|body\.|query\.)''', 'SSRF via URL interpolation'),
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
# File extensions to scan
|
||||
SCAN_EXTENSIONS = {'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'}
|
||||
|
||||
# Directories to skip
|
||||
SKIP_DIRS = {'node_modules', '.next', 'dist', 'build', '.git', 'coverage', '__pycache__'}
|
||||
|
||||
|
||||
def find_source_files(project_dir: str) -> list[str]:
|
||||
"""Find all source files to scan."""
|
||||
files = []
|
||||
for root, dirs, filenames in os.walk(project_dir):
|
||||
# Skip excluded directories
|
||||
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
|
||||
|
||||
for filename in filenames:
|
||||
ext = os.path.splitext(filename)[1]
|
||||
if ext in SCAN_EXTENSIONS:
|
||||
files.append(os.path.join(root, filename))
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def scan_file(file_path: str) -> list[SecurityIssue]:
|
||||
"""Scan a single file for security issues."""
|
||||
issues = []
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
content = f.read()
|
||||
lines = content.split('\n')
|
||||
except (IOError, OSError):
|
||||
return []
|
||||
|
||||
for category, config in SECURITY_PATTERNS.items():
|
||||
for pattern, title in config['patterns']:
|
||||
try:
|
||||
for match in re.finditer(pattern, content, re.IGNORECASE | re.MULTILINE):
|
||||
# Find line number
|
||||
line_start = content[:match.start()].count('\n') + 1
|
||||
line_content = lines[line_start - 1] if line_start <= len(lines) else ''
|
||||
|
||||
# Skip if in comment
|
||||
stripped = line_content.strip()
|
||||
if stripped.startswith('//') or stripped.startswith('*') or stripped.startswith('/*'):
|
||||
continue
|
||||
|
||||
# Skip if looks like env var reference
|
||||
if 'process.env' in line_content or 'import.meta.env' in line_content:
|
||||
continue
|
||||
|
||||
issues.append(SecurityIssue(
|
||||
severity=config['severity'],
|
||||
category=category,
|
||||
title=title,
|
||||
description=get_description(category),
|
||||
file_path=file_path,
|
||||
line_number=line_start,
|
||||
code_snippet=line_content.strip()[:100],
|
||||
recommendation=get_recommendation(category),
|
||||
cwe_id=config.get('cwe'),
|
||||
owasp_category=config.get('owasp')
|
||||
))
|
||||
except re.error:
|
||||
continue
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def get_description(category: str) -> str:
|
||||
"""Get detailed description for category."""
|
||||
descriptions = {
|
||||
'hardcoded_secrets': 'Credentials or secrets hardcoded in source code can be extracted by attackers.',
|
||||
'sql_injection': 'User input directly in SQL queries allows attackers to manipulate database operations.',
|
||||
'xss': 'Unsanitized user input rendered in HTML allows attackers to inject malicious scripts.',
|
||||
'path_traversal': 'User input in file paths allows attackers to access arbitrary files.',
|
||||
'command_injection': 'User input in system commands allows attackers to execute arbitrary commands.',
|
||||
'insecure_auth': 'Weak authentication mechanisms can be bypassed by attackers.',
|
||||
'sensitive_data_exposure': 'Sensitive information may be exposed through logs or errors.',
|
||||
'insecure_dependencies': 'Known vulnerable packages or weak cryptographic functions.',
|
||||
'cors_misconfiguration': 'Overly permissive CORS allows unauthorized cross-origin requests.',
|
||||
'insecure_randomness': 'Predictable random values can be guessed by attackers.',
|
||||
'debug_code': 'Debug code in production may expose sensitive information.',
|
||||
'nosql_injection': 'User input in NoSQL queries allows attackers to manipulate database operations.',
|
||||
'prototype_pollution': 'Modifying object prototypes can lead to code execution.',
|
||||
'ssrf': 'User-controlled URLs allow attackers to make requests to internal services.',
|
||||
}
|
||||
return descriptions.get(category, 'Security vulnerability detected.')
|
||||
|
||||
|
||||
def get_recommendation(category: str) -> str:
|
||||
"""Get remediation recommendation for category."""
|
||||
recommendations = {
|
||||
'hardcoded_secrets': 'Use environment variables (process.env) or a secrets manager.',
|
||||
'sql_injection': 'Use parameterized queries or ORM methods. Never concatenate user input.',
|
||||
'xss': 'Sanitize user input with DOMPurify or escape HTML entities.',
|
||||
'path_traversal': 'Validate and sanitize file paths. Use path.basename() and whitelist allowed paths.',
|
||||
'command_injection': 'Avoid shell commands with user input. Use execFile with argument arrays.',
|
||||
'insecure_auth': 'Use strong algorithms (RS256), hash passwords with bcrypt, enable all security flags.',
|
||||
'sensitive_data_exposure': 'Remove sensitive data from logs. Use generic error messages.',
|
||||
'insecure_dependencies': 'Update to latest secure versions. Use crypto.createCipheriv and SHA-256+.',
|
||||
'cors_misconfiguration': 'Specify exact allowed origins. Do not use wildcard with credentials.',
|
||||
'insecure_randomness': 'Use crypto.randomBytes() or crypto.randomUUID() for security-sensitive values.',
|
||||
'debug_code': 'Remove console statements and debugger in production builds.',
|
||||
'nosql_injection': 'Sanitize input and use schema validation. Avoid $where operators.',
|
||||
'prototype_pollution': 'Use Object.create(null) or validate/sanitize object keys.',
|
||||
'ssrf': 'Validate URLs against allowlist. Block internal IP ranges.',
|
||||
}
|
||||
return recommendations.get(category, 'Review and remediate the security issue.')
|
||||
|
||||
|
||||
def check_package_json(project_dir: str) -> list[SecurityIssue]:
|
||||
"""Check package.json for security issues."""
|
||||
issues = []
|
||||
pkg_path = os.path.join(project_dir, 'package.json')
|
||||
|
||||
if not os.path.exists(pkg_path):
|
||||
return []
|
||||
|
||||
try:
|
||||
with open(pkg_path, 'r') as f:
|
||||
pkg = json.load(f)
|
||||
except (json.JSONDecodeError, IOError):
|
||||
return []
|
||||
|
||||
# Known vulnerable packages (simplified check)
|
||||
vulnerable_packages = {
|
||||
'lodash': '< 4.17.21',
|
||||
'axios': '< 0.21.1',
|
||||
'node-fetch': '< 2.6.1',
|
||||
'minimist': '< 1.2.6',
|
||||
'serialize-javascript': '< 3.1.0',
|
||||
}
|
||||
|
||||
all_deps = {}
|
||||
all_deps.update(pkg.get('dependencies', {}))
|
||||
all_deps.update(pkg.get('devDependencies', {}))
|
||||
|
||||
for pkg_name in vulnerable_packages:
|
||||
if pkg_name in all_deps:
|
||||
issues.append(SecurityIssue(
|
||||
severity='MEDIUM',
|
||||
category='insecure_dependencies',
|
||||
title=f'Potentially vulnerable package: {pkg_name}',
|
||||
description=f'Package {pkg_name} may have known vulnerabilities. Run npm audit for details.',
|
||||
file_path=pkg_path,
|
||||
line_number=None,
|
||||
code_snippet=f'"{pkg_name}": "{all_deps[pkg_name]}"',
|
||||
recommendation='Run `npm audit` and update to the latest secure version.',
|
||||
cwe_id='CWE-1104',
|
||||
owasp_category='A06:2021-Vulnerable and Outdated Components'
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def check_env_files(project_dir: str) -> list[SecurityIssue]:
|
||||
"""Check for exposed environment files."""
|
||||
issues = []
|
||||
|
||||
env_files = ['.env', '.env.local', '.env.production', '.env.development']
|
||||
|
||||
for env_file in env_files:
|
||||
env_path = os.path.join(project_dir, env_file)
|
||||
if os.path.exists(env_path):
|
||||
# Check if in .gitignore
|
||||
gitignore_path = os.path.join(project_dir, '.gitignore')
|
||||
in_gitignore = False
|
||||
|
||||
if os.path.exists(gitignore_path):
|
||||
try:
|
||||
with open(gitignore_path, 'r') as f:
|
||||
gitignore_content = f.read()
|
||||
if env_file in gitignore_content or '.env*' in gitignore_content:
|
||||
in_gitignore = True
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
if not in_gitignore:
|
||||
issues.append(SecurityIssue(
|
||||
severity='HIGH',
|
||||
category='sensitive_data_exposure',
|
||||
title=f'Environment file not in .gitignore: {env_file}',
|
||||
description='Environment files containing secrets may be committed to version control.',
|
||||
file_path=env_path,
|
||||
line_number=None,
|
||||
code_snippet=env_file,
|
||||
recommendation=f'Add {env_file} to .gitignore immediately.',
|
||||
cwe_id='CWE-200',
|
||||
owasp_category='A02:2021-Cryptographic Failures'
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def run_scan(project_dir: str, min_severity: str = 'LOW') -> ScanResult:
|
||||
"""Run full security scan."""
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
severity_order = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO']
|
||||
min_severity_index = severity_order.index(min_severity.upper()) if min_severity.upper() in severity_order else 3
|
||||
|
||||
result = ScanResult()
|
||||
|
||||
# Find and scan source files
|
||||
files = find_source_files(project_dir)
|
||||
result.files_scanned = len(files)
|
||||
|
||||
for file_path in files:
|
||||
issues = scan_file(file_path)
|
||||
result.issues.extend(issues)
|
||||
|
||||
# Additional checks
|
||||
result.issues.extend(check_package_json(project_dir))
|
||||
result.issues.extend(check_env_files(project_dir))
|
||||
|
||||
# Filter by severity
|
||||
result.issues = [
|
||||
i for i in result.issues
|
||||
if severity_order.index(i.severity) <= min_severity_index
|
||||
]
|
||||
|
||||
# Sort by severity
|
||||
result.issues.sort(key=lambda x: severity_order.index(x.severity))
|
||||
|
||||
result.scan_duration = time.time() - start_time
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def format_report(result: ScanResult, format_type: str = 'text') -> str:
|
||||
"""Format scan results."""
|
||||
if format_type == 'json':
|
||||
return json.dumps({
|
||||
'files_scanned': result.files_scanned,
|
||||
'scan_duration': result.scan_duration,
|
||||
'total_issues': len(result.issues),
|
||||
'by_severity': {
|
||||
'CRITICAL': len([i for i in result.issues if i.severity == 'CRITICAL']),
|
||||
'HIGH': len([i for i in result.issues if i.severity == 'HIGH']),
|
||||
'MEDIUM': len([i for i in result.issues if i.severity == 'MEDIUM']),
|
||||
'LOW': len([i for i in result.issues if i.severity == 'LOW']),
|
||||
},
|
||||
'issues': [
|
||||
{
|
||||
'severity': i.severity,
|
||||
'category': i.category,
|
||||
'title': i.title,
|
||||
'description': i.description,
|
||||
'file_path': i.file_path,
|
||||
'line_number': i.line_number,
|
||||
'code_snippet': i.code_snippet,
|
||||
'recommendation': i.recommendation,
|
||||
'cwe_id': i.cwe_id,
|
||||
'owasp_category': i.owasp_category,
|
||||
}
|
||||
for i in result.issues
|
||||
]
|
||||
}, indent=2)
|
||||
|
||||
# Text format
|
||||
lines = []
|
||||
|
||||
# Header
|
||||
lines.append("")
|
||||
lines.append("=" * 80)
|
||||
lines.append(" SECURITY SCAN REPORT")
|
||||
lines.append("=" * 80)
|
||||
lines.append("")
|
||||
|
||||
# Summary
|
||||
critical = len([i for i in result.issues if i.severity == 'CRITICAL'])
|
||||
high = len([i for i in result.issues if i.severity == 'HIGH'])
|
||||
medium = len([i for i in result.issues if i.severity == 'MEDIUM'])
|
||||
low = len([i for i in result.issues if i.severity == 'LOW'])
|
||||
|
||||
lines.append("SUMMARY")
|
||||
lines.append("-" * 80)
|
||||
lines.append(f" Files scanned: {result.files_scanned}")
|
||||
lines.append(f" Scan duration: {result.scan_duration:.2f}s")
|
||||
lines.append(f" Total issues: {len(result.issues)}")
|
||||
lines.append("")
|
||||
lines.append(" By Severity:")
|
||||
lines.append(f" CRITICAL: {critical}")
|
||||
lines.append(f" HIGH: {high}")
|
||||
lines.append(f" MEDIUM: {medium}")
|
||||
lines.append(f" LOW: {low}")
|
||||
lines.append("")
|
||||
|
||||
# Issues by severity
|
||||
if result.issues:
|
||||
for severity in ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']:
|
||||
severity_issues = [i for i in result.issues if i.severity == severity]
|
||||
if severity_issues:
|
||||
icon = {'CRITICAL': '!!!', 'HIGH': '!!', 'MEDIUM': '!', 'LOW': '.'}[severity]
|
||||
lines.append(f"{icon} {severity} SEVERITY ISSUES ({len(severity_issues)})")
|
||||
lines.append("-" * 80)
|
||||
|
||||
for idx, issue in enumerate(severity_issues, 1):
|
||||
lines.append(f" [{idx}] {issue.title}")
|
||||
lines.append(f" Category: {issue.category}")
|
||||
if issue.file_path:
|
||||
loc = f"{issue.file_path}:{issue.line_number}" if issue.line_number else issue.file_path
|
||||
lines.append(f" Location: {loc}")
|
||||
if issue.code_snippet:
|
||||
lines.append(f" Code: {issue.code_snippet[:60]}...")
|
||||
if issue.cwe_id:
|
||||
lines.append(f" CWE: {issue.cwe_id}")
|
||||
if issue.owasp_category:
|
||||
lines.append(f" OWASP: {issue.owasp_category}")
|
||||
lines.append(f" Fix: {issue.recommendation}")
|
||||
lines.append("")
|
||||
else:
|
||||
lines.append("No security issues found!")
|
||||
lines.append("")
|
||||
|
||||
# Result
|
||||
lines.append("=" * 80)
|
||||
if critical > 0:
|
||||
lines.append(f" RESULT: CRITICAL ({critical} critical issues require immediate attention)")
|
||||
elif high > 0:
|
||||
lines.append(f" RESULT: FAIL ({high} high severity issues found)")
|
||||
elif medium > 0:
|
||||
lines.append(f" RESULT: WARNING ({medium} medium severity issues found)")
|
||||
elif low > 0:
|
||||
lines.append(f" RESULT: PASS WITH NOTES ({low} low severity issues)")
|
||||
else:
|
||||
lines.append(" RESULT: PASS (no security issues detected)")
|
||||
lines.append("=" * 80)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Security scanner for codebase")
|
||||
parser.add_argument("--project-dir", default=".", help="Project directory to scan")
|
||||
parser.add_argument("--severity", default="LOW",
|
||||
choices=['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'],
|
||||
help="Minimum severity to report")
|
||||
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
||||
parser.add_argument("--strict", action="store_true", help="Fail on any HIGH or above")
|
||||
args = parser.parse_args()
|
||||
|
||||
result = run_scan(args.project_dir, args.severity)
|
||||
|
||||
format_type = 'json' if args.json else 'text'
|
||||
print(format_report(result, format_type))
|
||||
|
||||
# Exit code
|
||||
critical = len([i for i in result.issues if i.severity == 'CRITICAL'])
|
||||
high = len([i for i in result.issues if i.severity == 'HIGH'])
|
||||
|
||||
if critical > 0:
|
||||
return 2 # Critical issues
|
||||
if args.strict and high > 0:
|
||||
return 1 # High issues in strict mode
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -0,0 +1,363 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Task management utilities for the guardrail workflow."""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# Try to import yaml, fall back to basic parsing if not available
|
||||
try:
|
||||
import yaml
|
||||
HAS_YAML = True
|
||||
except ImportError:
|
||||
HAS_YAML = False
|
||||
|
||||
|
||||
def parse_yaml_simple(content: str) -> dict:
|
||||
"""Simple YAML parser for basic task files."""
|
||||
result = {}
|
||||
current_key = None
|
||||
current_list = None
|
||||
|
||||
for line in content.split('\n'):
|
||||
line = line.rstrip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
|
||||
# Handle list items
|
||||
if line.startswith(' - '):
|
||||
if current_list is not None:
|
||||
current_list.append(line[4:].strip())
|
||||
continue
|
||||
|
||||
# Handle key-value pairs
|
||||
if ':' in line and not line.startswith(' '):
|
||||
key, _, value = line.partition(':')
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
|
||||
if value:
|
||||
result[key] = value
|
||||
current_list = None
|
||||
else:
|
||||
result[key] = []
|
||||
current_list = result[key]
|
||||
current_key = key
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def load_yaml(filepath: str) -> dict:
|
||||
"""Load YAML file."""
|
||||
with open(filepath, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
if HAS_YAML:
|
||||
return yaml.safe_load(content) or {}
|
||||
return parse_yaml_simple(content)
|
||||
|
||||
|
||||
def save_yaml(filepath: str, data: dict):
|
||||
"""Save data to YAML file."""
|
||||
if HAS_YAML:
|
||||
with open(filepath, 'w') as f:
|
||||
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
|
||||
else:
|
||||
# Simple YAML writer
|
||||
lines = []
|
||||
for key, value in data.items():
|
||||
if isinstance(value, list):
|
||||
lines.append(f"{key}:")
|
||||
for item in value:
|
||||
lines.append(f" - {item}")
|
||||
elif isinstance(value, str) and '\n' in value:
|
||||
lines.append(f"{key}: |")
|
||||
for line in value.split('\n'):
|
||||
lines.append(f" {line}")
|
||||
else:
|
||||
lines.append(f"{key}: {value}")
|
||||
|
||||
with open(filepath, 'w') as f:
|
||||
f.write('\n'.join(lines))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Version-Aware Task Directory
|
||||
# ============================================================================
|
||||
|
||||
def get_workflow_dir() -> Path:
|
||||
"""Get the .workflow directory path."""
|
||||
return Path('.workflow')
|
||||
|
||||
|
||||
def get_current_tasks_dir() -> str:
|
||||
"""Get the tasks directory for the currently active workflow version.
|
||||
|
||||
Returns the version-specific tasks directory if a workflow is active,
|
||||
otherwise falls back to 'tasks' for backward compatibility.
|
||||
"""
|
||||
current_path = get_workflow_dir() / 'current.yml'
|
||||
if not current_path.exists():
|
||||
return 'tasks' # Fallback for no active workflow
|
||||
|
||||
current = load_yaml(str(current_path))
|
||||
version = current.get('active_version')
|
||||
if not version:
|
||||
return 'tasks' # Fallback
|
||||
|
||||
tasks_dir = get_workflow_dir() / 'versions' / version / 'tasks'
|
||||
tasks_dir.mkdir(parents=True, exist_ok=True)
|
||||
return str(tasks_dir)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Task Operations
|
||||
# ============================================================================
|
||||
|
||||
def find_tasks(tasks_dir: str, filters: dict = None) -> list:
|
||||
"""Find all task files matching filters."""
|
||||
tasks = []
|
||||
tasks_path = Path(tasks_dir)
|
||||
|
||||
if not tasks_path.exists():
|
||||
return tasks
|
||||
|
||||
for filepath in tasks_path.glob('**/*.yml'):
|
||||
try:
|
||||
task = load_yaml(str(filepath))
|
||||
task['_filepath'] = str(filepath)
|
||||
|
||||
# Apply filters
|
||||
if filters:
|
||||
match = True
|
||||
for key, value in filters.items():
|
||||
if task.get(key) != value:
|
||||
match = False
|
||||
break
|
||||
if not match:
|
||||
continue
|
||||
|
||||
tasks.append(task)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not parse {filepath}: {e}", file=sys.stderr)
|
||||
|
||||
return tasks
|
||||
|
||||
|
||||
def list_tasks(tasks_dir: str, status: str = None, agent: str = None):
|
||||
"""List tasks with optional filtering."""
|
||||
filters = {}
|
||||
if status:
|
||||
filters['status'] = status
|
||||
if agent:
|
||||
filters['agent'] = agent
|
||||
|
||||
tasks = find_tasks(tasks_dir, filters)
|
||||
|
||||
if not tasks:
|
||||
print("No tasks found.")
|
||||
return
|
||||
|
||||
# Group by status
|
||||
by_status = {}
|
||||
for task in tasks:
|
||||
s = task.get('status', 'unknown')
|
||||
if s not in by_status:
|
||||
by_status[s] = []
|
||||
by_status[s].append(task)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("TASK LIST")
|
||||
print("=" * 60)
|
||||
|
||||
status_order = ['pending', 'in_progress', 'review', 'approved', 'completed', 'blocked']
|
||||
for s in status_order:
|
||||
if s in by_status:
|
||||
print(f"\n{s.upper()} ({len(by_status[s])})")
|
||||
print("-" * 40)
|
||||
for task in by_status[s]:
|
||||
agent = task.get('agent', '?')
|
||||
priority = task.get('priority', 'medium')
|
||||
print(f" [{agent}] {task.get('id', 'unknown')} ({priority})")
|
||||
print(f" {task.get('title', 'No title')}")
|
||||
|
||||
|
||||
def get_next_task(tasks_dir: str, agent: str) -> dict:
|
||||
"""Get next available task for an agent."""
|
||||
tasks = find_tasks(tasks_dir, {'agent': agent, 'status': 'pending'})
|
||||
|
||||
if not tasks:
|
||||
return None
|
||||
|
||||
# Sort by priority (high > medium > low)
|
||||
priority_order = {'high': 0, 'medium': 1, 'low': 2}
|
||||
tasks.sort(key=lambda t: priority_order.get(t.get('priority', 'medium'), 1))
|
||||
|
||||
# Check dependencies
|
||||
for task in tasks:
|
||||
deps = task.get('dependencies', [])
|
||||
if not deps:
|
||||
return task
|
||||
|
||||
# Check if all dependencies are completed
|
||||
all_deps_done = True
|
||||
for dep_id in deps:
|
||||
dep_tasks = find_tasks(tasks_dir, {'id': dep_id})
|
||||
if dep_tasks and dep_tasks[0].get('status') != 'completed':
|
||||
all_deps_done = False
|
||||
break
|
||||
|
||||
if all_deps_done:
|
||||
return task
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def update_task_status(tasks_dir: str, task_id: str, new_status: str, notes: str = None):
|
||||
"""Update task status."""
|
||||
tasks = find_tasks(tasks_dir, {'id': task_id})
|
||||
|
||||
if not tasks:
|
||||
print(f"Error: Task {task_id} not found")
|
||||
return False
|
||||
|
||||
task = tasks[0]
|
||||
filepath = task['_filepath']
|
||||
|
||||
# Remove internal field
|
||||
del task['_filepath']
|
||||
|
||||
# Update status
|
||||
task['status'] = new_status
|
||||
|
||||
if new_status == 'completed':
|
||||
task['completed_at'] = datetime.now().isoformat()
|
||||
|
||||
if notes:
|
||||
task['review_notes'] = notes
|
||||
|
||||
save_yaml(filepath, task)
|
||||
print(f"Updated {task_id} to {new_status}")
|
||||
return True
|
||||
|
||||
|
||||
def complete_all_tasks(tasks_dir: str):
|
||||
"""Mark all non-completed tasks as completed."""
|
||||
tasks = find_tasks(tasks_dir)
|
||||
completed_count = 0
|
||||
|
||||
for task in tasks:
|
||||
if task.get('status') != 'completed':
|
||||
filepath = task['_filepath']
|
||||
del task['_filepath']
|
||||
task['status'] = 'completed'
|
||||
task['completed_at'] = datetime.now().isoformat()
|
||||
save_yaml(filepath, task)
|
||||
completed_count += 1
|
||||
print(f" Completed: {task.get('id', 'unknown')}")
|
||||
|
||||
print(f"\nMarked {completed_count} task(s) as completed.")
|
||||
return completed_count
|
||||
|
||||
|
||||
def show_status(tasks_dir: str, manifest_path: str):
|
||||
"""Show overall workflow status."""
|
||||
tasks = find_tasks(tasks_dir)
|
||||
|
||||
# Count by status
|
||||
status_counts = {}
|
||||
agent_counts = {'frontend': {'pending': 0, 'completed': 0},
|
||||
'backend': {'pending': 0, 'completed': 0},
|
||||
'reviewer': {'pending': 0}}
|
||||
|
||||
for task in tasks:
|
||||
s = task.get('status', 'unknown')
|
||||
status_counts[s] = status_counts.get(s, 0) + 1
|
||||
|
||||
agent = task.get('agent', 'unknown')
|
||||
if agent in agent_counts:
|
||||
if s == 'pending':
|
||||
agent_counts[agent]['pending'] += 1
|
||||
elif s == 'completed':
|
||||
if 'completed' in agent_counts[agent]:
|
||||
agent_counts[agent]['completed'] += 1
|
||||
|
||||
print("\n" + "╔" + "═" * 58 + "╗")
|
||||
print("║" + "WORKFLOW STATUS".center(58) + "║")
|
||||
print("╠" + "═" * 58 + "╣")
|
||||
print("║" + " TASKS BY STATUS".ljust(58) + "║")
|
||||
print("║" + f" ⏳ Pending: {status_counts.get('pending', 0)}".ljust(58) + "║")
|
||||
print("║" + f" 🔄 In Progress: {status_counts.get('in_progress', 0)}".ljust(58) + "║")
|
||||
print("║" + f" 🔍 Review: {status_counts.get('review', 0)}".ljust(58) + "║")
|
||||
print("║" + f" ✅ Approved: {status_counts.get('approved', 0)}".ljust(58) + "║")
|
||||
print("║" + f" ✓ Completed: {status_counts.get('completed', 0)}".ljust(58) + "║")
|
||||
print("║" + f" 🚫 Blocked: {status_counts.get('blocked', 0)}".ljust(58) + "║")
|
||||
print("╠" + "═" * 58 + "╣")
|
||||
print("║" + " TASKS BY AGENT".ljust(58) + "║")
|
||||
print("║" + f" 🎨 Frontend: {agent_counts['frontend']['pending']} pending, {agent_counts['frontend']['completed']} completed".ljust(58) + "║")
|
||||
print("║" + f" ⚙️ Backend: {agent_counts['backend']['pending']} pending, {agent_counts['backend']['completed']} completed".ljust(58) + "║")
|
||||
print("║" + f" 🔍 Reviewer: {agent_counts['reviewer']['pending']} pending".ljust(58) + "║")
|
||||
print("╚" + "═" * 58 + "╝")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Task management for guardrail workflow")
|
||||
subparsers = parser.add_subparsers(dest='command', help='Commands')
|
||||
|
||||
# list command
|
||||
list_parser = subparsers.add_parser('list', help='List tasks')
|
||||
list_parser.add_argument('--status', help='Filter by status')
|
||||
list_parser.add_argument('--agent', help='Filter by agent')
|
||||
list_parser.add_argument('--tasks-dir', default=None, help='Tasks directory (defaults to current version)')
|
||||
|
||||
# next command
|
||||
next_parser = subparsers.add_parser('next', help='Get next task for agent')
|
||||
next_parser.add_argument('agent', choices=['frontend', 'backend', 'reviewer'])
|
||||
next_parser.add_argument('--tasks-dir', default=None, help='Tasks directory (defaults to current version)')
|
||||
|
||||
# update command
|
||||
update_parser = subparsers.add_parser('update', help='Update task status')
|
||||
update_parser.add_argument('task_id', help='Task ID')
|
||||
update_parser.add_argument('status', choices=['pending', 'in_progress', 'review', 'approved', 'completed', 'blocked'])
|
||||
update_parser.add_argument('--notes', help='Review notes')
|
||||
update_parser.add_argument('--tasks-dir', default=None, help='Tasks directory (defaults to current version)')
|
||||
|
||||
# status command
|
||||
status_parser = subparsers.add_parser('status', help='Show workflow status')
|
||||
status_parser.add_argument('--tasks-dir', default=None, help='Tasks directory (defaults to current version)')
|
||||
status_parser.add_argument('--manifest', default='project_manifest.json', help='Manifest path')
|
||||
|
||||
# complete-all command
|
||||
complete_all_parser = subparsers.add_parser('complete-all', help='Mark all tasks as completed')
|
||||
complete_all_parser.add_argument('--tasks-dir', default=None, help='Tasks directory (defaults to current version)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Resolve tasks_dir to version-specific directory if not explicitly provided
|
||||
if hasattr(args, 'tasks_dir') and args.tasks_dir is None:
|
||||
args.tasks_dir = get_current_tasks_dir()
|
||||
|
||||
if args.command == 'list':
|
||||
list_tasks(args.tasks_dir, args.status, args.agent)
|
||||
elif args.command == 'next':
|
||||
task = get_next_task(args.tasks_dir, args.agent)
|
||||
if task:
|
||||
print(f"Next task for {args.agent}: {task.get('id')}")
|
||||
print(f" Title: {task.get('title')}")
|
||||
print(f" Files: {task.get('file_paths', [])}")
|
||||
else:
|
||||
print(f"No pending tasks for {args.agent}")
|
||||
elif args.command == 'update':
|
||||
update_task_status(args.tasks_dir, args.task_id, args.status, args.notes)
|
||||
elif args.command == 'status':
|
||||
show_status(args.tasks_dir, args.manifest)
|
||||
elif args.command == 'complete-all':
|
||||
complete_all_tasks(args.tasks_dir)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,715 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Task State Manager for parallel execution and dependency tracking.
|
||||
|
||||
Manages task-level states independently from workflow phase, enabling:
|
||||
- Multiple tasks in_progress simultaneously (if no blocking dependencies)
|
||||
- Dependency validation before task execution
|
||||
- Task grouping by agent type for parallel frontend/backend work
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Set, Tuple
|
||||
|
||||
# Try to import yaml
|
||||
try:
|
||||
import yaml
|
||||
HAS_YAML = True
|
||||
except ImportError:
|
||||
HAS_YAML = False
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# YAML Helpers
|
||||
# ============================================================================
|
||||
|
||||
def load_yaml(filepath: str) -> dict:
|
||||
"""Load YAML file."""
|
||||
if not os.path.exists(filepath):
|
||||
return {}
|
||||
with open(filepath, 'r') as f:
|
||||
content = f.read()
|
||||
if not content.strip():
|
||||
return {}
|
||||
if HAS_YAML:
|
||||
return yaml.safe_load(content) or {}
|
||||
return parse_simple_yaml(content)
|
||||
|
||||
|
||||
def parse_simple_yaml(content: str) -> dict:
|
||||
"""Parse simple YAML without PyYAML dependency."""
|
||||
result = {}
|
||||
current_key = None
|
||||
current_list = None
|
||||
|
||||
for line in content.split('\n'):
|
||||
stripped = line.strip()
|
||||
|
||||
if not stripped or stripped.startswith('#'):
|
||||
continue
|
||||
|
||||
if stripped.startswith('- '):
|
||||
if current_list is not None:
|
||||
value = stripped[2:].strip()
|
||||
if (value.startswith('"') and value.endswith('"')) or \
|
||||
(value.startswith("'") and value.endswith("'")):
|
||||
value = value[1:-1]
|
||||
current_list.append(value)
|
||||
continue
|
||||
|
||||
if ':' in stripped:
|
||||
key, _, value = stripped.partition(':')
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
|
||||
if value == '' or value == '[]':
|
||||
current_key = key
|
||||
current_list = []
|
||||
result[key] = current_list
|
||||
elif value == '{}':
|
||||
result[key] = {}
|
||||
current_list = None
|
||||
elif value == 'null' or value == '~':
|
||||
result[key] = None
|
||||
current_list = None
|
||||
elif value == 'true':
|
||||
result[key] = True
|
||||
current_list = None
|
||||
elif value == 'false':
|
||||
result[key] = False
|
||||
current_list = None
|
||||
elif value.isdigit():
|
||||
result[key] = int(value)
|
||||
current_list = None
|
||||
else:
|
||||
if (value.startswith('"') and value.endswith('"')) or \
|
||||
(value.startswith("'") and value.endswith("'")):
|
||||
value = value[1:-1]
|
||||
result[key] = value
|
||||
current_list = None
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def save_yaml(filepath: str, data: dict):
|
||||
"""Save data to YAML file."""
|
||||
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||
if HAS_YAML:
|
||||
with open(filepath, 'w') as f:
|
||||
yaml.dump(data, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
||||
else:
|
||||
with open(filepath, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Path Helpers
|
||||
# ============================================================================
|
||||
|
||||
def get_workflow_dir() -> Path:
|
||||
return Path('.workflow')
|
||||
|
||||
|
||||
def get_current_state_path() -> Path:
|
||||
return get_workflow_dir() / 'current.yml'
|
||||
|
||||
|
||||
def get_active_version() -> Optional[str]:
|
||||
"""Get the currently active workflow version."""
|
||||
current_path = get_current_state_path()
|
||||
if not current_path.exists():
|
||||
return None
|
||||
current = load_yaml(str(current_path))
|
||||
return current.get('active_version')
|
||||
|
||||
|
||||
def get_tasks_dir() -> Optional[Path]:
|
||||
"""Get the tasks directory for the active version."""
|
||||
version = get_active_version()
|
||||
if not version:
|
||||
return None
|
||||
tasks_dir = get_workflow_dir() / 'versions' / version / 'tasks'
|
||||
tasks_dir.mkdir(parents=True, exist_ok=True)
|
||||
return tasks_dir
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Task State Constants
|
||||
# ============================================================================
|
||||
|
||||
TASK_STATES = ['pending', 'in_progress', 'review', 'approved', 'completed', 'blocked']
|
||||
|
||||
VALID_TASK_TRANSITIONS = {
|
||||
'pending': ['in_progress', 'blocked'],
|
||||
'in_progress': ['review', 'blocked', 'pending'], # Can go back if paused
|
||||
'review': ['approved', 'in_progress'], # Can go back if changes needed
|
||||
'approved': ['completed'],
|
||||
'completed': [], # Terminal state
|
||||
'blocked': ['pending'] # Unblocked when dependencies resolve
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Task Loading
|
||||
# ============================================================================
|
||||
|
||||
def load_all_tasks() -> Dict[str, dict]:
|
||||
"""Load all tasks from the current version's tasks directory."""
|
||||
tasks_dir = get_tasks_dir()
|
||||
if not tasks_dir or not tasks_dir.exists():
|
||||
return {}
|
||||
|
||||
tasks = {}
|
||||
for task_file in tasks_dir.glob('*.yml'):
|
||||
task_id = task_file.stem
|
||||
task = load_yaml(str(task_file))
|
||||
if task:
|
||||
tasks[task_id] = task
|
||||
|
||||
return tasks
|
||||
|
||||
|
||||
def load_task(task_id: str) -> Optional[dict]:
|
||||
"""Load a single task by ID."""
|
||||
tasks_dir = get_tasks_dir()
|
||||
if not tasks_dir:
|
||||
return None
|
||||
|
||||
task_path = tasks_dir / f"{task_id}.yml"
|
||||
if not task_path.exists():
|
||||
return None
|
||||
|
||||
return load_yaml(str(task_path))
|
||||
|
||||
|
||||
def save_task(task: dict):
|
||||
"""Save a task to the tasks directory."""
|
||||
tasks_dir = get_tasks_dir()
|
||||
if not tasks_dir:
|
||||
print("Error: No active workflow")
|
||||
return
|
||||
|
||||
task_id = task.get('id', task.get('task_id'))
|
||||
if not task_id:
|
||||
print("Error: Task has no ID")
|
||||
return
|
||||
|
||||
task['updated_at'] = datetime.now().isoformat()
|
||||
save_yaml(str(tasks_dir / f"{task_id}.yml"), task)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Dependency Resolution
|
||||
# ============================================================================
|
||||
|
||||
def get_task_dependencies(task: dict) -> List[str]:
|
||||
"""Get the list of task IDs that this task depends on."""
|
||||
return task.get('dependencies', []) or []
|
||||
|
||||
|
||||
def check_dependencies_met(task_id: str, all_tasks: Dict[str, dict]) -> Tuple[bool, List[str]]:
|
||||
"""
|
||||
Check if all dependencies for a task are completed.
|
||||
|
||||
Returns:
|
||||
Tuple of (all_met, unmet_dependency_ids)
|
||||
"""
|
||||
task = all_tasks.get(task_id)
|
||||
if not task:
|
||||
return False, [f"Task {task_id} not found"]
|
||||
|
||||
dependencies = get_task_dependencies(task)
|
||||
unmet = []
|
||||
|
||||
for dep_id in dependencies:
|
||||
dep_task = all_tasks.get(dep_id)
|
||||
if not dep_task:
|
||||
unmet.append(f"{dep_id} (not found)")
|
||||
elif dep_task.get('status') not in ['completed', 'approved']:
|
||||
unmet.append(f"{dep_id} (status: {dep_task.get('status', 'unknown')})")
|
||||
|
||||
return len(unmet) == 0, unmet
|
||||
|
||||
|
||||
def get_dependency_graph(all_tasks: Dict[str, dict]) -> Dict[str, Set[str]]:
|
||||
"""Build a dependency graph for all tasks."""
|
||||
graph = {}
|
||||
for task_id, task in all_tasks.items():
|
||||
deps = get_task_dependencies(task)
|
||||
graph[task_id] = set(deps)
|
||||
return graph
|
||||
|
||||
|
||||
def detect_circular_dependencies(all_tasks: Dict[str, dict]) -> List[List[str]]:
|
||||
"""Detect circular dependencies using DFS."""
|
||||
graph = get_dependency_graph(all_tasks)
|
||||
cycles = []
|
||||
visited = set()
|
||||
rec_stack = set()
|
||||
|
||||
def dfs(node: str, path: List[str]) -> bool:
|
||||
visited.add(node)
|
||||
rec_stack.add(node)
|
||||
path.append(node)
|
||||
|
||||
for neighbor in graph.get(node, set()):
|
||||
if neighbor not in visited:
|
||||
if dfs(neighbor, path):
|
||||
return True
|
||||
elif neighbor in rec_stack:
|
||||
# Found cycle
|
||||
cycle_start = path.index(neighbor)
|
||||
cycles.append(path[cycle_start:] + [neighbor])
|
||||
return True
|
||||
|
||||
path.pop()
|
||||
rec_stack.remove(node)
|
||||
return False
|
||||
|
||||
for node in graph:
|
||||
if node not in visited:
|
||||
dfs(node, [])
|
||||
|
||||
return cycles
|
||||
|
||||
|
||||
def get_execution_order(all_tasks: Dict[str, dict]) -> List[str]:
|
||||
"""Get topologically sorted execution order respecting dependencies."""
|
||||
graph = get_dependency_graph(all_tasks)
|
||||
|
||||
# Kahn's algorithm for topological sort
|
||||
in_degree = {task_id: 0 for task_id in all_tasks}
|
||||
for deps in graph.values():
|
||||
for dep in deps:
|
||||
if dep in in_degree:
|
||||
in_degree[dep] += 1
|
||||
|
||||
queue = [t for t, d in in_degree.items() if d == 0]
|
||||
result = []
|
||||
|
||||
while queue:
|
||||
node = queue.pop(0)
|
||||
result.append(node)
|
||||
|
||||
for other, deps in graph.items():
|
||||
if node in deps:
|
||||
in_degree[other] -= 1
|
||||
if in_degree[other] == 0:
|
||||
queue.append(other)
|
||||
|
||||
# Reverse since we want dependencies first
|
||||
return list(reversed(result))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Parallel Execution Support
|
||||
# ============================================================================
|
||||
|
||||
def get_parallel_candidates(all_tasks: Dict[str, dict]) -> Dict[str, List[str]]:
|
||||
"""
|
||||
Get tasks that can be executed in parallel, grouped by agent.
|
||||
|
||||
Returns:
|
||||
Dict mapping agent type to list of task IDs ready for parallel execution
|
||||
"""
|
||||
candidates = {'frontend': [], 'backend': [], 'other': []}
|
||||
|
||||
for task_id, task in all_tasks.items():
|
||||
status = task.get('status', 'pending')
|
||||
|
||||
# Only consider pending tasks
|
||||
if status != 'pending':
|
||||
continue
|
||||
|
||||
# Check if dependencies are met
|
||||
deps_met, _ = check_dependencies_met(task_id, all_tasks)
|
||||
if not deps_met:
|
||||
continue
|
||||
|
||||
# Group by agent
|
||||
agent = task.get('agent', 'other')
|
||||
if agent in candidates:
|
||||
candidates[agent].append(task_id)
|
||||
else:
|
||||
candidates['other'].append(task_id)
|
||||
|
||||
return candidates
|
||||
|
||||
|
||||
def get_active_tasks() -> Dict[str, List[str]]:
|
||||
"""
|
||||
Get currently active (in_progress) tasks grouped by agent.
|
||||
|
||||
Returns:
|
||||
Dict mapping agent type to list of active task IDs
|
||||
"""
|
||||
all_tasks = load_all_tasks()
|
||||
active = {'frontend': [], 'backend': [], 'other': []}
|
||||
|
||||
for task_id, task in all_tasks.items():
|
||||
if task.get('status') == 'in_progress':
|
||||
agent = task.get('agent', 'other')
|
||||
if agent in active:
|
||||
active[agent].append(task_id)
|
||||
else:
|
||||
active['other'].append(task_id)
|
||||
|
||||
return active
|
||||
|
||||
|
||||
def can_start_task(task_id: str, max_per_agent: int = 1) -> Tuple[bool, str]:
|
||||
"""
|
||||
Check if a task can be started given current active tasks.
|
||||
|
||||
Args:
|
||||
task_id: Task to check
|
||||
max_per_agent: Maximum concurrent tasks per agent type
|
||||
|
||||
Returns:
|
||||
Tuple of (can_start, reason)
|
||||
"""
|
||||
all_tasks = load_all_tasks()
|
||||
task = all_tasks.get(task_id)
|
||||
|
||||
if not task:
|
||||
return False, f"Task {task_id} not found"
|
||||
|
||||
status = task.get('status', 'pending')
|
||||
if status != 'pending':
|
||||
return False, f"Task is not pending (status: {status})"
|
||||
|
||||
# Check dependencies
|
||||
deps_met, unmet = check_dependencies_met(task_id, all_tasks)
|
||||
if not deps_met:
|
||||
return False, f"Dependencies not met: {', '.join(unmet)}"
|
||||
|
||||
# Check concurrent task limit per agent
|
||||
agent = task.get('agent', 'other')
|
||||
active = get_active_tasks()
|
||||
if len(active.get(agent, [])) >= max_per_agent:
|
||||
return False, f"Max concurrent {agent} tasks reached ({max_per_agent})"
|
||||
|
||||
return True, "Ready to start"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# State Transitions
|
||||
# ============================================================================
|
||||
|
||||
def transition_task(task_id: str, new_status: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
Transition a task to a new status with validation.
|
||||
|
||||
Returns:
|
||||
Tuple of (success, message)
|
||||
"""
|
||||
task = load_task(task_id)
|
||||
if not task:
|
||||
return False, f"Task {task_id} not found"
|
||||
|
||||
current_status = task.get('status', 'pending')
|
||||
|
||||
# Validate transition
|
||||
valid_next = VALID_TASK_TRANSITIONS.get(current_status, [])
|
||||
if new_status not in valid_next:
|
||||
return False, f"Invalid transition: {current_status} → {new_status}. Valid: {valid_next}"
|
||||
|
||||
# For in_progress, check dependencies
|
||||
if new_status == 'in_progress':
|
||||
all_tasks = load_all_tasks()
|
||||
deps_met, unmet = check_dependencies_met(task_id, all_tasks)
|
||||
if not deps_met:
|
||||
# Block instead
|
||||
task['status'] = 'blocked'
|
||||
task['blocked_by'] = unmet
|
||||
task['blocked_at'] = datetime.now().isoformat()
|
||||
save_task(task)
|
||||
return False, f"Dependencies not met, task blocked: {', '.join(unmet)}"
|
||||
|
||||
# Perform transition
|
||||
task['status'] = new_status
|
||||
task[f'{new_status}_at'] = datetime.now().isoformat()
|
||||
|
||||
# Clear blocked info if unblocking
|
||||
if current_status == 'blocked' and new_status == 'pending':
|
||||
task.pop('blocked_by', None)
|
||||
task.pop('blocked_at', None)
|
||||
|
||||
save_task(task)
|
||||
return True, f"Task {task_id}: {current_status} → {new_status}"
|
||||
|
||||
|
||||
def update_blocked_tasks():
|
||||
"""Check and unblock tasks whose dependencies are now met."""
|
||||
all_tasks = load_all_tasks()
|
||||
unblocked = []
|
||||
|
||||
for task_id, task in all_tasks.items():
|
||||
if task.get('status') != 'blocked':
|
||||
continue
|
||||
|
||||
deps_met, _ = check_dependencies_met(task_id, all_tasks)
|
||||
if deps_met:
|
||||
success, msg = transition_task(task_id, 'pending')
|
||||
if success:
|
||||
unblocked.append(task_id)
|
||||
|
||||
return unblocked
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Status Report
|
||||
# ============================================================================
|
||||
|
||||
def get_status_summary() -> dict:
|
||||
"""Get summary of task statuses."""
|
||||
all_tasks = load_all_tasks()
|
||||
|
||||
summary = {
|
||||
'total': len(all_tasks),
|
||||
'by_status': {status: 0 for status in TASK_STATES},
|
||||
'by_agent': {},
|
||||
'blocked_details': [],
|
||||
'ready_for_parallel': get_parallel_candidates(all_tasks)
|
||||
}
|
||||
|
||||
for task_id, task in all_tasks.items():
|
||||
status = task.get('status', 'pending')
|
||||
agent = task.get('agent', 'other')
|
||||
|
||||
summary['by_status'][status] = summary['by_status'].get(status, 0) + 1
|
||||
|
||||
if agent not in summary['by_agent']:
|
||||
summary['by_agent'][agent] = {'total': 0, 'by_status': {}}
|
||||
summary['by_agent'][agent]['total'] += 1
|
||||
summary['by_agent'][agent]['by_status'][status] = \
|
||||
summary['by_agent'][agent]['by_status'].get(status, 0) + 1
|
||||
|
||||
if status == 'blocked':
|
||||
summary['blocked_details'].append({
|
||||
'task_id': task_id,
|
||||
'blocked_by': task.get('blocked_by', []),
|
||||
'blocked_at': task.get('blocked_at')
|
||||
})
|
||||
|
||||
return summary
|
||||
|
||||
|
||||
def show_status():
|
||||
"""Display task status summary."""
|
||||
summary = get_status_summary()
|
||||
|
||||
print()
|
||||
print("╔" + "═" * 60 + "╗")
|
||||
print("║" + "TASK STATE MANAGER STATUS".center(60) + "║")
|
||||
print("╠" + "═" * 60 + "╣")
|
||||
print("║" + f" Total Tasks: {summary['total']}".ljust(60) + "║")
|
||||
print("╠" + "═" * 60 + "╣")
|
||||
print("║" + " BY STATUS".ljust(60) + "║")
|
||||
|
||||
status_icons = {
|
||||
'pending': '⏳', 'in_progress': '🔄', 'review': '🔍',
|
||||
'approved': '✅', 'completed': '✓', 'blocked': '🚫'
|
||||
}
|
||||
|
||||
for status, count in summary['by_status'].items():
|
||||
icon = status_icons.get(status, '•')
|
||||
print("║" + f" {icon} {status}: {count}".ljust(60) + "║")
|
||||
|
||||
print("╠" + "═" * 60 + "╣")
|
||||
print("║" + " BY AGENT".ljust(60) + "║")
|
||||
|
||||
for agent, data in summary['by_agent'].items():
|
||||
print("║" + f" {agent}: {data['total']} tasks".ljust(60) + "║")
|
||||
for status, count in data['by_status'].items():
|
||||
if count > 0:
|
||||
print("║" + f" └─ {status}: {count}".ljust(60) + "║")
|
||||
|
||||
# Show parallel candidates
|
||||
parallel = summary['ready_for_parallel']
|
||||
has_parallel = any(len(v) > 0 for v in parallel.values())
|
||||
|
||||
if has_parallel:
|
||||
print("╠" + "═" * 60 + "╣")
|
||||
print("║" + " 🔀 READY FOR PARALLEL EXECUTION".ljust(60) + "║")
|
||||
for agent, tasks in parallel.items():
|
||||
if tasks:
|
||||
print("║" + f" {agent}: {', '.join(tasks[:3])}".ljust(60) + "║")
|
||||
if len(tasks) > 3:
|
||||
print("║" + f" (+{len(tasks) - 3} more)".ljust(60) + "║")
|
||||
|
||||
# Show blocked tasks
|
||||
if summary['blocked_details']:
|
||||
print("╠" + "═" * 60 + "╣")
|
||||
print("║" + " 🚫 BLOCKED TASKS".ljust(60) + "║")
|
||||
for blocked in summary['blocked_details'][:5]:
|
||||
deps = ', '.join(blocked['blocked_by'][:2])
|
||||
if len(blocked['blocked_by']) > 2:
|
||||
deps += f" (+{len(blocked['blocked_by']) - 2})"
|
||||
print("║" + f" {blocked['task_id']}".ljust(60) + "║")
|
||||
print("║" + f" Blocked by: {deps}".ljust(60) + "║")
|
||||
|
||||
print("╚" + "═" * 60 + "╝")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CLI Interface
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Task state management for parallel execution")
|
||||
subparsers = parser.add_subparsers(dest='command', help='Commands')
|
||||
|
||||
# status command
|
||||
subparsers.add_parser('status', help='Show task status summary')
|
||||
|
||||
# transition command
|
||||
trans_parser = subparsers.add_parser('transition', help='Transition task status')
|
||||
trans_parser.add_argument('task_id', help='Task ID')
|
||||
trans_parser.add_argument('status', choices=TASK_STATES, help='New status')
|
||||
|
||||
# can-start command
|
||||
can_start_parser = subparsers.add_parser('can-start', help='Check if task can start')
|
||||
can_start_parser.add_argument('task_id', help='Task ID')
|
||||
can_start_parser.add_argument('--max-per-agent', type=int, default=1,
|
||||
help='Max concurrent tasks per agent')
|
||||
|
||||
# parallel command
|
||||
subparsers.add_parser('parallel', help='Show tasks ready for parallel execution')
|
||||
|
||||
# deps command
|
||||
deps_parser = subparsers.add_parser('deps', help='Show task dependencies')
|
||||
deps_parser.add_argument('task_id', nargs='?', help='Task ID (optional)')
|
||||
|
||||
# check-deps command
|
||||
check_deps_parser = subparsers.add_parser('check-deps', help='Check if dependencies are met')
|
||||
check_deps_parser.add_argument('task_id', help='Task ID')
|
||||
|
||||
# unblock command
|
||||
subparsers.add_parser('unblock', help='Update blocked tasks whose deps are now met')
|
||||
|
||||
# order command
|
||||
subparsers.add_parser('order', help='Show execution order respecting dependencies')
|
||||
|
||||
# cycles command
|
||||
subparsers.add_parser('cycles', help='Detect circular dependencies')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == 'status':
|
||||
show_status()
|
||||
|
||||
elif args.command == 'transition':
|
||||
success, msg = transition_task(args.task_id, args.status)
|
||||
print(msg)
|
||||
if not success:
|
||||
sys.exit(1)
|
||||
|
||||
elif args.command == 'can-start':
|
||||
can_start, reason = can_start_task(args.task_id, args.max_per_agent)
|
||||
print(f"{'✅ Yes' if can_start else '❌ No'}: {reason}")
|
||||
sys.exit(0 if can_start else 1)
|
||||
|
||||
elif args.command == 'parallel':
|
||||
all_tasks = load_all_tasks()
|
||||
candidates = get_parallel_candidates(all_tasks)
|
||||
|
||||
print("\n🔀 Tasks Ready for Parallel Execution:\n")
|
||||
for agent, tasks in candidates.items():
|
||||
if tasks:
|
||||
print(f" {agent}:")
|
||||
for task_id in tasks:
|
||||
task = all_tasks.get(task_id, {})
|
||||
print(f" - {task_id}: {task.get('title', 'No title')}")
|
||||
|
||||
if not any(candidates.values()):
|
||||
print(" No tasks ready for parallel execution")
|
||||
|
||||
elif args.command == 'deps':
|
||||
all_tasks = load_all_tasks()
|
||||
|
||||
if args.task_id:
|
||||
task = all_tasks.get(args.task_id)
|
||||
if task:
|
||||
deps = get_task_dependencies(task)
|
||||
print(f"\n{args.task_id} depends on:")
|
||||
if deps:
|
||||
for dep_id in deps:
|
||||
dep = all_tasks.get(dep_id, {})
|
||||
status = dep.get('status', 'unknown')
|
||||
print(f" - {dep_id} ({status})")
|
||||
else:
|
||||
print(" (no dependencies)")
|
||||
else:
|
||||
print(f"Task {args.task_id} not found")
|
||||
else:
|
||||
# Show all dependencies
|
||||
graph = get_dependency_graph(all_tasks)
|
||||
print("\nDependency Graph:\n")
|
||||
for task_id, deps in graph.items():
|
||||
if deps:
|
||||
print(f" {task_id} ← {', '.join(deps)}")
|
||||
|
||||
elif args.command == 'check-deps':
|
||||
all_tasks = load_all_tasks()
|
||||
deps_met, unmet = check_dependencies_met(args.task_id, all_tasks)
|
||||
|
||||
if deps_met:
|
||||
print(f"✅ All dependencies met for {args.task_id}")
|
||||
else:
|
||||
print(f"❌ Unmet dependencies for {args.task_id}:")
|
||||
for dep in unmet:
|
||||
print(f" - {dep}")
|
||||
|
||||
sys.exit(0 if deps_met else 1)
|
||||
|
||||
elif args.command == 'unblock':
|
||||
unblocked = update_blocked_tasks()
|
||||
if unblocked:
|
||||
print(f"✅ Unblocked {len(unblocked)} tasks:")
|
||||
for task_id in unblocked:
|
||||
print(f" - {task_id}")
|
||||
else:
|
||||
print("No tasks to unblock")
|
||||
|
||||
elif args.command == 'order':
|
||||
all_tasks = load_all_tasks()
|
||||
|
||||
# Check for cycles first
|
||||
cycles = detect_circular_dependencies(all_tasks)
|
||||
if cycles:
|
||||
print("⚠️ Cannot determine order - circular dependencies detected!")
|
||||
for cycle in cycles:
|
||||
print(f" Cycle: {' → '.join(cycle)}")
|
||||
sys.exit(1)
|
||||
|
||||
order = get_execution_order(all_tasks)
|
||||
print("\n📋 Execution Order (respecting dependencies):\n")
|
||||
for i, task_id in enumerate(order, 1):
|
||||
task = all_tasks.get(task_id, {})
|
||||
status = task.get('status', 'pending')
|
||||
agent = task.get('agent', '?')
|
||||
print(f" {i}. [{agent}] {task_id} ({status})")
|
||||
|
||||
elif args.command == 'cycles':
|
||||
all_tasks = load_all_tasks()
|
||||
cycles = detect_circular_dependencies(all_tasks)
|
||||
|
||||
if cycles:
|
||||
print("⚠️ Circular dependencies detected:\n")
|
||||
for cycle in cycles:
|
||||
print(f" {' → '.join(cycle)}")
|
||||
else:
|
||||
print("✅ No circular dependencies detected")
|
||||
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Transition project between phases."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
VALID_PHASES = ["DESIGN_PHASE", "DESIGN_REVIEW", "IMPLEMENTATION_PHASE"]
|
||||
VALID_TRANSITIONS = {
|
||||
"DESIGN_PHASE": ["DESIGN_REVIEW"],
|
||||
"DESIGN_REVIEW": ["DESIGN_PHASE", "IMPLEMENTATION_PHASE"],
|
||||
"IMPLEMENTATION_PHASE": ["DESIGN_PHASE"]
|
||||
}
|
||||
|
||||
|
||||
def load_manifest(manifest_path: str) -> dict:
|
||||
"""Load manifest."""
|
||||
with open(manifest_path) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def save_manifest(manifest_path: str, manifest: dict):
|
||||
"""Save manifest."""
|
||||
with open(manifest_path, "w") as f:
|
||||
json.dump(manifest, f, indent=2)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Transition project phase")
|
||||
parser.add_argument("--to", required=True, choices=VALID_PHASES, help="Target phase")
|
||||
parser.add_argument("--manifest", default="project_manifest.json", help="Manifest path")
|
||||
args = parser.parse_args()
|
||||
|
||||
manifest_path = args.manifest
|
||||
if not os.path.isabs(manifest_path):
|
||||
manifest_path = os.path.join(os.getcwd(), manifest_path)
|
||||
|
||||
if not os.path.exists(manifest_path):
|
||||
print(f"Error: Manifest not found at {manifest_path}")
|
||||
return 1
|
||||
|
||||
manifest = load_manifest(manifest_path)
|
||||
current_phase = manifest["state"]["current_phase"]
|
||||
target_phase = args.to
|
||||
|
||||
if target_phase not in VALID_TRANSITIONS.get(current_phase, []):
|
||||
print(f"Error: Cannot transition from {current_phase} to {target_phase}")
|
||||
print(f"Valid transitions: {VALID_TRANSITIONS.get(current_phase, [])}")
|
||||
return 1
|
||||
|
||||
# Update phase
|
||||
manifest["state"]["current_phase"] = target_phase
|
||||
|
||||
# Add to history
|
||||
manifest["state"]["revision_history"].append({
|
||||
"action": "PHASE_TRANSITION",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"details": f"Transitioned from {current_phase} to {target_phase}"
|
||||
})
|
||||
|
||||
# If transitioning to implementation, mark entities as approved
|
||||
if target_phase == "IMPLEMENTATION_PHASE":
|
||||
for entity_type in ["pages", "components", "api_endpoints", "database_tables"]:
|
||||
for entity in manifest["entities"].get(entity_type, []):
|
||||
if entity.get("status") == "DEFINED":
|
||||
entity["status"] = "APPROVED"
|
||||
|
||||
save_manifest(manifest_path, manifest)
|
||||
print(f"Transitioned to {target_phase}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
|
|
@ -0,0 +1,381 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Validate Implementation Against API Contract
|
||||
|
||||
This script verifies that both backend and frontend implementations
|
||||
comply with the generated API contract.
|
||||
|
||||
Checks performed:
|
||||
1. Backend routes exist and export correct HTTP methods
|
||||
2. Frontend components import from shared types file
|
||||
3. API calls use correct paths and methods
|
||||
4. Types are properly imported (not recreated locally)
|
||||
|
||||
Exit codes:
|
||||
0 = All validations pass
|
||||
1 = Warnings found (non-critical violations)
|
||||
2 = Critical violations (missing routes, type mismatches)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any, Tuple, Optional
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
yaml = None
|
||||
|
||||
|
||||
def load_yaml(path: Path) -> Dict:
|
||||
"""Load YAML file."""
|
||||
if yaml:
|
||||
with open(path) as f:
|
||||
return yaml.safe_load(f)
|
||||
else:
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
try:
|
||||
return json.loads(content)
|
||||
except:
|
||||
return {}
|
||||
|
||||
|
||||
def find_project_root(start_dir: Path) -> Path:
|
||||
"""Find project root by looking for package.json."""
|
||||
current = start_dir.resolve()
|
||||
while current != current.parent:
|
||||
if (current / 'package.json').exists():
|
||||
return current
|
||||
current = current.parent
|
||||
return start_dir
|
||||
|
||||
|
||||
class ContractValidator:
|
||||
"""Validates implementation against API contract."""
|
||||
|
||||
def __init__(self, contract_path: Path, project_dir: Path):
|
||||
self.contract_path = contract_path
|
||||
self.project_dir = project_dir
|
||||
self.contract = load_yaml(contract_path)
|
||||
self.violations: List[Dict[str, Any]] = []
|
||||
self.warnings: List[Dict[str, Any]] = []
|
||||
|
||||
def validate_all(self) -> Tuple[int, List[Dict], List[Dict]]:
|
||||
"""
|
||||
Run all validations.
|
||||
|
||||
Returns:
|
||||
Tuple of (exit_code, violations, warnings)
|
||||
"""
|
||||
# Validate backend
|
||||
self.validate_backend_routes()
|
||||
self.validate_backend_type_imports()
|
||||
|
||||
# Validate frontend
|
||||
self.validate_frontend_type_imports()
|
||||
self.validate_frontend_api_calls()
|
||||
|
||||
# Determine exit code
|
||||
critical_count = len([v for v in self.violations if v.get('severity') == 'critical'])
|
||||
warning_count = len(self.warnings)
|
||||
|
||||
if critical_count > 0:
|
||||
return 2, self.violations, self.warnings
|
||||
elif len(self.violations) > 0:
|
||||
return 1, self.violations, self.warnings
|
||||
else:
|
||||
return 0, self.violations, self.warnings
|
||||
|
||||
def validate_backend_routes(self) -> None:
|
||||
"""Validate that all backend routes from contract exist."""
|
||||
backend_routes = self.contract.get('backend_routes', [])
|
||||
|
||||
for route in backend_routes:
|
||||
file_path = self.project_dir / route['file_path']
|
||||
endpoint_id = route.get('endpoint_id', 'unknown')
|
||||
export_name = route.get('export_name', 'GET')
|
||||
|
||||
if not file_path.exists():
|
||||
self.violations.append({
|
||||
'type': 'missing_route',
|
||||
'severity': 'critical',
|
||||
'endpoint_id': endpoint_id,
|
||||
'expected_file': str(route['file_path']),
|
||||
'message': f"Backend route file missing: {route['file_path']}",
|
||||
})
|
||||
continue
|
||||
|
||||
# Check if file exports the correct HTTP method
|
||||
content = file_path.read_text()
|
||||
|
||||
# Check for Next.js App Router pattern: export async function GET/POST/etc.
|
||||
export_pattern = rf'export\s+(async\s+)?function\s+{export_name}\s*\('
|
||||
if not re.search(export_pattern, content):
|
||||
# Also check for const exports: export const GET = ...
|
||||
const_pattern = rf'export\s+const\s+{export_name}\s*='
|
||||
if not re.search(const_pattern, content):
|
||||
self.violations.append({
|
||||
'type': 'missing_export',
|
||||
'severity': 'critical',
|
||||
'endpoint_id': endpoint_id,
|
||||
'file': str(route['file_path']),
|
||||
'expected_export': export_name,
|
||||
'message': f"Route {route['file_path']} missing {export_name} export",
|
||||
})
|
||||
|
||||
def validate_backend_type_imports(self) -> None:
|
||||
"""Validate backend files import from shared types."""
|
||||
backend_routes = self.contract.get('backend_routes', [])
|
||||
|
||||
for route in backend_routes:
|
||||
file_path = self.project_dir / route['file_path']
|
||||
if not file_path.exists():
|
||||
continue # Already reported as missing
|
||||
|
||||
content = file_path.read_text()
|
||||
|
||||
# Check for import from @/types/api or ./types/api or ../types/api
|
||||
import_patterns = [
|
||||
r"import\s+.*from\s+['\"]@/types/api['\"]",
|
||||
r"import\s+.*from\s+['\"]\.+/types/api['\"]",
|
||||
r"import\s+type\s+.*from\s+['\"]@/types/api['\"]",
|
||||
]
|
||||
|
||||
has_import = any(re.search(p, content) for p in import_patterns)
|
||||
|
||||
if not has_import:
|
||||
self.warnings.append({
|
||||
'type': 'missing_type_import',
|
||||
'severity': 'warning',
|
||||
'file': str(route['file_path']),
|
||||
'message': f"Backend route {route['file_path']} should import types from @/types/api",
|
||||
})
|
||||
|
||||
# Check for local type declarations that might conflict
|
||||
local_type_patterns = [
|
||||
r'(interface|type)\s+User\s*[={]',
|
||||
r'(interface|type)\s+.*Request\s*[={]',
|
||||
r'(interface|type)\s+.*Response\s*[={]',
|
||||
]
|
||||
|
||||
for pattern in local_type_patterns:
|
||||
match = re.search(pattern, content)
|
||||
if match and 'import' not in content[:match.start()].split('\n')[-1]:
|
||||
self.warnings.append({
|
||||
'type': 'local_type_definition',
|
||||
'severity': 'warning',
|
||||
'file': str(route['file_path']),
|
||||
'message': f"Backend route defines local types. Should import from @/types/api instead.",
|
||||
})
|
||||
break
|
||||
|
||||
def validate_frontend_type_imports(self) -> None:
|
||||
"""Validate frontend files import from shared types."""
|
||||
frontend_calls = self.contract.get('frontend_calls', [])
|
||||
|
||||
checked_files = set()
|
||||
|
||||
for call in frontend_calls:
|
||||
file_path_str = call.get('source', {}).get('file_path', '')
|
||||
if not file_path_str or file_path_str in checked_files:
|
||||
continue
|
||||
|
||||
checked_files.add(file_path_str)
|
||||
file_path = self.project_dir / file_path_str
|
||||
|
||||
if not file_path.exists():
|
||||
# Check alternate paths (page vs component)
|
||||
if '/components/' in file_path_str:
|
||||
alt_path = file_path_str.replace('/components/', '/app/components/')
|
||||
file_path = self.project_dir / alt_path
|
||||
if not file_path.exists():
|
||||
self.violations.append({
|
||||
'type': 'missing_frontend_file',
|
||||
'severity': 'high',
|
||||
'expected_file': file_path_str,
|
||||
'message': f"Frontend file missing: {file_path_str}",
|
||||
})
|
||||
continue
|
||||
else:
|
||||
self.violations.append({
|
||||
'type': 'missing_frontend_file',
|
||||
'severity': 'high',
|
||||
'expected_file': file_path_str,
|
||||
'message': f"Frontend file missing: {file_path_str}",
|
||||
})
|
||||
continue
|
||||
|
||||
content = file_path.read_text()
|
||||
|
||||
# Check for import from @/types/api
|
||||
import_patterns = [
|
||||
r"import\s+.*from\s+['\"]@/types/api['\"]",
|
||||
r"import\s+.*from\s+['\"]\.+/types/api['\"]",
|
||||
r"import\s+type\s+.*from\s+['\"]@/types/api['\"]",
|
||||
]
|
||||
|
||||
has_import = any(re.search(p, content) for p in import_patterns)
|
||||
|
||||
if not has_import:
|
||||
self.warnings.append({
|
||||
'type': 'missing_type_import',
|
||||
'severity': 'warning',
|
||||
'file': file_path_str,
|
||||
'message': f"Frontend file {file_path_str} should import types from @/types/api",
|
||||
})
|
||||
|
||||
def validate_frontend_api_calls(self) -> None:
|
||||
"""Validate frontend API calls match contract."""
|
||||
frontend_calls = self.contract.get('frontend_calls', [])
|
||||
endpoints = {e['id']: e for e in self.contract.get('endpoints', [])}
|
||||
|
||||
for call in frontend_calls:
|
||||
file_path_str = call.get('source', {}).get('file_path', '')
|
||||
endpoint_id = call.get('endpoint_id', '')
|
||||
|
||||
if not file_path_str or endpoint_id not in endpoints:
|
||||
continue
|
||||
|
||||
file_path = self.project_dir / file_path_str
|
||||
|
||||
# Try alternate paths
|
||||
if not file_path.exists():
|
||||
if '/components/' in file_path_str:
|
||||
alt_path = file_path_str.replace('/components/', '/app/components/')
|
||||
file_path = self.project_dir / alt_path
|
||||
|
||||
if not file_path.exists():
|
||||
continue # Already reported
|
||||
|
||||
content = file_path.read_text()
|
||||
endpoint = endpoints[endpoint_id]
|
||||
expected_method = endpoint.get('method', 'GET')
|
||||
expected_path = endpoint.get('path', '')
|
||||
|
||||
# Check for API call to this endpoint
|
||||
# Look for fetch calls or axios calls
|
||||
fetch_patterns = [
|
||||
rf"fetch\s*\(\s*['\"`][^'\"]*{re.escape(expected_path)}",
|
||||
rf"fetch\s*\(\s*API_PATHS\.",
|
||||
rf"axios\.{expected_method.lower()}\s*\(",
|
||||
]
|
||||
|
||||
has_call = any(re.search(p, content, re.IGNORECASE) for p in fetch_patterns)
|
||||
|
||||
# If component is supposed to call this API but doesn't, it might be a dynamic call
|
||||
# or using a different pattern - this is a soft warning
|
||||
# The important validation is that when they DO call, they use correct types
|
||||
|
||||
def validate_types_file_exists(self) -> bool:
|
||||
"""Check if shared types file exists."""
|
||||
types_file = self.project_dir / 'app' / 'types' / 'api.ts'
|
||||
if not types_file.exists():
|
||||
self.violations.append({
|
||||
'type': 'missing_types_file',
|
||||
'severity': 'critical',
|
||||
'expected_file': 'app/types/api.ts',
|
||||
'message': "Shared types file missing: app/types/api.ts",
|
||||
})
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def print_report(violations: List[Dict], warnings: List[Dict]) -> None:
|
||||
"""Print validation report."""
|
||||
print("\n" + "=" * 60)
|
||||
print("API CONTRACT VALIDATION REPORT")
|
||||
print("=" * 60)
|
||||
|
||||
if not violations and not warnings:
|
||||
print("\n✅ ALL VALIDATIONS PASSED")
|
||||
print("\nBoth frontend and backend implementations comply with the API contract.")
|
||||
return
|
||||
|
||||
if violations:
|
||||
print(f"\n❌ VIOLATIONS FOUND: {len(violations)}")
|
||||
print("-" * 40)
|
||||
|
||||
critical = [v for v in violations if v.get('severity') == 'critical']
|
||||
high = [v for v in violations if v.get('severity') == 'high']
|
||||
other = [v for v in violations if v.get('severity') not in ['critical', 'high']]
|
||||
|
||||
if critical:
|
||||
print("\n🔴 CRITICAL (Must fix):")
|
||||
for v in critical:
|
||||
print(f" • {v['message']}")
|
||||
if 'expected_file' in v:
|
||||
print(f" Expected: {v['expected_file']}")
|
||||
|
||||
if high:
|
||||
print("\n🟠 HIGH (Should fix):")
|
||||
for v in high:
|
||||
print(f" • {v['message']}")
|
||||
|
||||
if other:
|
||||
print("\n🟡 OTHER:")
|
||||
for v in other:
|
||||
print(f" • {v['message']}")
|
||||
|
||||
if warnings:
|
||||
print(f"\n⚠️ WARNINGS: {len(warnings)}")
|
||||
print("-" * 40)
|
||||
for w in warnings:
|
||||
print(f" • {w['message']}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
if any(v.get('severity') == 'critical' for v in violations):
|
||||
print("VERDICT: ❌ FAILED - Critical violations must be fixed")
|
||||
elif violations:
|
||||
print("VERDICT: ⚠️ WARNINGS - Review and fix if possible")
|
||||
else:
|
||||
print("VERDICT: ✅ PASSED with warnings")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: validate_against_contract.py <api_contract.yml> [--project-dir <dir>]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
contract_path = Path(sys.argv[1])
|
||||
|
||||
# Parse project directory
|
||||
project_dir = Path('.')
|
||||
if '--project-dir' in sys.argv:
|
||||
idx = sys.argv.index('--project-dir')
|
||||
project_dir = Path(sys.argv[idx + 1])
|
||||
|
||||
project_dir = find_project_root(project_dir)
|
||||
|
||||
if not contract_path.exists():
|
||||
print(f"Error: Contract file not found: {contract_path}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
# Run validation
|
||||
validator = ContractValidator(contract_path, project_dir)
|
||||
|
||||
# First check types file exists
|
||||
validator.validate_types_file_exists()
|
||||
|
||||
# Run all validations
|
||||
exit_code, violations, warnings = validator.validate_all()
|
||||
|
||||
# Print report
|
||||
print_report(violations, warnings)
|
||||
|
||||
# Summary stats
|
||||
print(f"\nValidation complete:")
|
||||
print(f" Backend routes checked: {len(validator.contract.get('backend_routes', []))}")
|
||||
print(f" Frontend calls checked: {len(validator.contract.get('frontend_calls', []))}")
|
||||
print(f" Types defined: {len(validator.contract.get('types', []))}")
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,536 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
API Contract Validator for guardrail workflow.
|
||||
|
||||
Validates that frontend API calls match backend endpoint definitions:
|
||||
- Endpoints exist
|
||||
- HTTP methods match
|
||||
- Request/response structures align
|
||||
|
||||
Usage:
|
||||
python3 validate_api_contract.py --manifest project_manifest.json --project-dir .
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class APICall(NamedTuple):
|
||||
"""Frontend API call."""
|
||||
file_path: str
|
||||
line_number: int
|
||||
endpoint: str
|
||||
method: str
|
||||
has_body: bool
|
||||
raw_line: str
|
||||
|
||||
|
||||
class APIEndpoint(NamedTuple):
|
||||
"""Backend API endpoint."""
|
||||
file_path: str
|
||||
endpoint: str
|
||||
method: str
|
||||
has_request_body: bool
|
||||
response_type: str
|
||||
|
||||
|
||||
class ContractIssue(NamedTuple):
|
||||
"""API contract violation."""
|
||||
severity: str # ERROR, WARNING
|
||||
category: str
|
||||
message: str
|
||||
file_path: str
|
||||
line_number: int | None
|
||||
suggestion: str
|
||||
|
||||
|
||||
def load_manifest(manifest_path: str) -> dict | None:
|
||||
"""Load manifest if exists."""
|
||||
if not os.path.exists(manifest_path):
|
||||
return None
|
||||
try:
|
||||
with open(manifest_path) as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, IOError):
|
||||
return None
|
||||
|
||||
|
||||
def find_frontend_files(project_dir: str) -> list[str]:
|
||||
"""Find frontend source files."""
|
||||
frontend_patterns = [
|
||||
'app/**/*.tsx', 'app/**/*.ts',
|
||||
'src/**/*.tsx', 'src/**/*.ts',
|
||||
'pages/**/*.tsx', 'pages/**/*.ts',
|
||||
'components/**/*.tsx', 'components/**/*.ts',
|
||||
'hooks/**/*.ts', 'hooks/**/*.tsx',
|
||||
'lib/**/*.ts', 'lib/**/*.tsx',
|
||||
'services/**/*.ts', 'services/**/*.tsx',
|
||||
]
|
||||
|
||||
# Exclude patterns
|
||||
exclude_patterns = ['node_modules', '.next', 'dist', 'build', 'api']
|
||||
|
||||
files = []
|
||||
for pattern in frontend_patterns:
|
||||
base_dir = pattern.split('/')[0]
|
||||
search_dir = Path(project_dir) / base_dir
|
||||
if search_dir.exists():
|
||||
for file_path in search_dir.rglob('*.ts*'):
|
||||
path_str = str(file_path)
|
||||
if not any(ex in path_str for ex in exclude_patterns):
|
||||
# Skip API route files
|
||||
if '/api/' not in path_str:
|
||||
files.append(path_str)
|
||||
|
||||
return list(set(files))
|
||||
|
||||
|
||||
def find_backend_files(project_dir: str) -> list[str]:
|
||||
"""Find backend API route files."""
|
||||
backend_patterns = [
|
||||
'app/api/**/*.ts', 'app/api/**/*.tsx',
|
||||
'pages/api/**/*.ts', 'pages/api/**/*.tsx',
|
||||
'api/**/*.ts',
|
||||
'src/api/**/*.ts',
|
||||
'server/**/*.ts',
|
||||
'routes/**/*.ts',
|
||||
]
|
||||
|
||||
files = []
|
||||
for pattern in backend_patterns:
|
||||
base_parts = pattern.split('/')
|
||||
search_dir = Path(project_dir)
|
||||
for part in base_parts[:-1]:
|
||||
if '*' not in part:
|
||||
search_dir = search_dir / part
|
||||
|
||||
if search_dir.exists():
|
||||
for file_path in search_dir.rglob('*.ts*'):
|
||||
path_str = str(file_path)
|
||||
if 'node_modules' not in path_str:
|
||||
files.append(path_str)
|
||||
|
||||
return list(set(files))
|
||||
|
||||
|
||||
def extract_frontend_api_calls(file_path: str) -> list[APICall]:
|
||||
"""Extract API calls from frontend file."""
|
||||
calls = []
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
lines = content.split('\n')
|
||||
except (IOError, UnicodeDecodeError):
|
||||
return []
|
||||
|
||||
# Patterns for API calls
|
||||
patterns = [
|
||||
# fetch('/api/...', { method: 'POST', body: ... })
|
||||
(r'''fetch\s*\(\s*['"](/api/[^'"]+)['"]''', 'fetch'),
|
||||
# axios.get('/api/...'), axios.post('/api/...', data)
|
||||
(r'''axios\.(get|post|put|patch|delete)\s*\(\s*['"](/api/[^'"]+)['"]''', 'axios'),
|
||||
# api.get('/users'), api.post('/users', data)
|
||||
(r'''api\.(get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]''', 'api_client'),
|
||||
# useSWR('/api/...'), useSWR(() => '/api/...')
|
||||
(r'''useSWR\s*\(\s*['"](/api/[^'"]+)['"]''', 'swr'),
|
||||
# useQuery(['key'], () => fetch('/api/...'))
|
||||
(r'''fetch\s*\(\s*[`'"](/api/[^`'"]+)[`'"]''', 'fetch_template'),
|
||||
]
|
||||
|
||||
for line_num, line in enumerate(lines, 1):
|
||||
for pattern, call_type in patterns:
|
||||
matches = re.finditer(pattern, line, re.IGNORECASE)
|
||||
for match in matches:
|
||||
groups = match.groups()
|
||||
|
||||
if call_type == 'fetch' or call_type == 'swr' or call_type == 'fetch_template':
|
||||
endpoint = groups[0]
|
||||
# Try to detect method from options
|
||||
method = 'GET'
|
||||
if 'method' in line.lower():
|
||||
method_match = re.search(r'''method:\s*['"](\w+)['"]''', line, re.IGNORECASE)
|
||||
if method_match:
|
||||
method = method_match.group(1).upper()
|
||||
has_body = 'body:' in line.lower() or 'body=' in line.lower()
|
||||
|
||||
elif call_type == 'axios' or call_type == 'api_client':
|
||||
method = groups[0].upper()
|
||||
endpoint = groups[1]
|
||||
# POST, PUT, PATCH typically have body
|
||||
has_body = method in ['POST', 'PUT', 'PATCH']
|
||||
else:
|
||||
continue
|
||||
|
||||
# Normalize endpoint
|
||||
if not endpoint.startswith('/api/'):
|
||||
endpoint = f'/api/{endpoint.lstrip("/")}'
|
||||
|
||||
calls.append(APICall(
|
||||
file_path=file_path,
|
||||
line_number=line_num,
|
||||
endpoint=endpoint,
|
||||
method=method,
|
||||
has_body=has_body,
|
||||
raw_line=line.strip()
|
||||
))
|
||||
|
||||
return calls
|
||||
|
||||
|
||||
def extract_backend_endpoints(file_path: str) -> list[APIEndpoint]:
|
||||
"""Extract API endpoints from backend file."""
|
||||
endpoints = []
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
except (IOError, UnicodeDecodeError):
|
||||
return []
|
||||
|
||||
# Determine endpoint from file path (Next.js App Router / Pages Router)
|
||||
rel_path = file_path
|
||||
if '/app/api/' in file_path:
|
||||
# App Router: app/api/users/route.ts -> /api/users
|
||||
api_path = re.search(r'/app/api/(.+?)/(route|page)\.(ts|tsx|js|jsx)', file_path)
|
||||
if api_path:
|
||||
endpoint = f'/api/{api_path.group(1)}'
|
||||
else:
|
||||
api_path = re.search(r'/app/api/(.+?)\.(ts|tsx|js|jsx)', file_path)
|
||||
if api_path:
|
||||
endpoint = f'/api/{api_path.group(1)}'
|
||||
else:
|
||||
endpoint = '/api/unknown'
|
||||
elif '/pages/api/' in file_path:
|
||||
# Pages Router: pages/api/users.ts -> /api/users
|
||||
api_path = re.search(r'/pages/api/(.+?)\.(ts|tsx|js|jsx)', file_path)
|
||||
if api_path:
|
||||
endpoint = f'/api/{api_path.group(1)}'
|
||||
else:
|
||||
endpoint = '/api/unknown'
|
||||
else:
|
||||
endpoint = '/api/unknown'
|
||||
|
||||
# Clean up dynamic segments: [id] -> :id
|
||||
endpoint = re.sub(r'\[(\w+)\]', r':\1', endpoint)
|
||||
|
||||
# Detect HTTP methods
|
||||
# Next.js App Router exports: GET, POST, PUT, DELETE, PATCH
|
||||
app_router_methods = re.findall(
|
||||
r'export\s+(?:async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)',
|
||||
content
|
||||
)
|
||||
|
||||
# Pages Router: req.method checks
|
||||
pages_router_methods = re.findall(
|
||||
r'''req\.method\s*===?\s*['"](\w+)['"]''',
|
||||
content
|
||||
)
|
||||
|
||||
# Express-style: router.get, router.post, app.get, app.post
|
||||
express_methods = re.findall(
|
||||
r'''(?:router|app)\.(get|post|put|patch|delete)\s*\(''',
|
||||
content,
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
methods = set()
|
||||
methods.update(m.upper() for m in app_router_methods)
|
||||
methods.update(m.upper() for m in pages_router_methods)
|
||||
methods.update(m.upper() for m in express_methods)
|
||||
|
||||
# Default to GET if no methods detected
|
||||
if not methods:
|
||||
methods = {'GET'}
|
||||
|
||||
# Detect request body handling
|
||||
has_body_patterns = [
|
||||
r'request\.json\(\)',
|
||||
r'req\.body',
|
||||
r'await\s+request\.json',
|
||||
r'JSON\.parse',
|
||||
r'body\s*:',
|
||||
]
|
||||
has_request_body = any(re.search(p, content) for p in has_body_patterns)
|
||||
|
||||
# Detect response type
|
||||
response_type = 'json' # default
|
||||
if 'NextResponse.json' in content or 'res.json' in content:
|
||||
response_type = 'json'
|
||||
elif 'new Response(' in content:
|
||||
response_type = 'response'
|
||||
elif 'res.send' in content:
|
||||
response_type = 'text'
|
||||
|
||||
for method in methods:
|
||||
endpoints.append(APIEndpoint(
|
||||
file_path=file_path,
|
||||
endpoint=endpoint,
|
||||
method=method,
|
||||
has_request_body=has_request_body,
|
||||
response_type=response_type
|
||||
))
|
||||
|
||||
return endpoints
|
||||
|
||||
|
||||
def normalize_endpoint(endpoint: str) -> str:
|
||||
"""Normalize endpoint for comparison."""
|
||||
# Remove query params
|
||||
endpoint = endpoint.split('?')[0]
|
||||
# Normalize dynamic segments
|
||||
endpoint = re.sub(r':\w+', ':param', endpoint)
|
||||
endpoint = re.sub(r'\$\{[^}]+\}', ':param', endpoint)
|
||||
# Remove trailing slash
|
||||
endpoint = endpoint.rstrip('/')
|
||||
return endpoint.lower()
|
||||
|
||||
|
||||
def match_endpoints(call_endpoint: str, api_endpoint: str) -> bool:
|
||||
"""Check if frontend call matches backend endpoint."""
|
||||
norm_call = normalize_endpoint(call_endpoint)
|
||||
norm_api = normalize_endpoint(api_endpoint)
|
||||
|
||||
# Exact match
|
||||
if norm_call == norm_api:
|
||||
return True
|
||||
|
||||
# Pattern match with dynamic segments
|
||||
api_pattern = re.sub(r':param', r'[^/]+', norm_api)
|
||||
if re.match(f'^{api_pattern}$', norm_call):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def validate_api_contract(
|
||||
project_dir: str,
|
||||
manifest: dict | None = None
|
||||
) -> tuple[list[ContractIssue], dict]:
|
||||
"""Validate API contract between frontend and backend."""
|
||||
issues = []
|
||||
stats = {
|
||||
'frontend_calls': 0,
|
||||
'backend_endpoints': 0,
|
||||
'matched': 0,
|
||||
'unmatched_calls': 0,
|
||||
'method_mismatches': 0,
|
||||
'body_mismatches': 0,
|
||||
}
|
||||
|
||||
# Find files
|
||||
frontend_files = find_frontend_files(project_dir)
|
||||
backend_files = find_backend_files(project_dir)
|
||||
|
||||
# Extract API calls and endpoints
|
||||
all_calls: list[APICall] = []
|
||||
all_endpoints: list[APIEndpoint] = []
|
||||
|
||||
for file in frontend_files:
|
||||
all_calls.extend(extract_frontend_api_calls(file))
|
||||
|
||||
for file in backend_files:
|
||||
all_endpoints.extend(extract_backend_endpoints(file))
|
||||
|
||||
stats['frontend_calls'] = len(all_calls)
|
||||
stats['backend_endpoints'] = len(all_endpoints)
|
||||
|
||||
# Build endpoint lookup
|
||||
endpoint_map: dict[str, list[APIEndpoint]] = {}
|
||||
for ep in all_endpoints:
|
||||
key = normalize_endpoint(ep.endpoint)
|
||||
if key not in endpoint_map:
|
||||
endpoint_map[key] = []
|
||||
endpoint_map[key].append(ep)
|
||||
|
||||
# Validate each frontend call
|
||||
for call in all_calls:
|
||||
matched = False
|
||||
|
||||
for ep in all_endpoints:
|
||||
if match_endpoints(call.endpoint, ep.endpoint):
|
||||
matched = True
|
||||
|
||||
# Check method match
|
||||
if call.method != ep.method:
|
||||
# Check if endpoint supports this method
|
||||
endpoint_methods = [e.method for e in all_endpoints
|
||||
if match_endpoints(call.endpoint, e.endpoint)]
|
||||
if call.method not in endpoint_methods:
|
||||
issues.append(ContractIssue(
|
||||
severity='ERROR',
|
||||
category='METHOD_MISMATCH',
|
||||
message=f"Frontend calls {call.method} {call.endpoint} but backend only supports {endpoint_methods}",
|
||||
file_path=call.file_path,
|
||||
line_number=call.line_number,
|
||||
suggestion=f"Change method to one of: {', '.join(endpoint_methods)}"
|
||||
))
|
||||
stats['method_mismatches'] += 1
|
||||
continue
|
||||
|
||||
# Check body requirements
|
||||
if call.has_body and not ep.has_request_body:
|
||||
issues.append(ContractIssue(
|
||||
severity='WARNING',
|
||||
category='BODY_MISMATCH',
|
||||
message=f"Frontend sends body to {call.endpoint} but backend may not process it",
|
||||
file_path=call.file_path,
|
||||
line_number=call.line_number,
|
||||
suggestion="Verify backend handles request body or remove body from frontend call"
|
||||
))
|
||||
stats['body_mismatches'] += 1
|
||||
|
||||
if not call.has_body and ep.has_request_body and ep.method in ['POST', 'PUT', 'PATCH']:
|
||||
issues.append(ContractIssue(
|
||||
severity='WARNING',
|
||||
category='MISSING_BODY',
|
||||
message=f"Backend expects body for {call.method} {call.endpoint} but frontend may not send it",
|
||||
file_path=call.file_path,
|
||||
line_number=call.line_number,
|
||||
suggestion="Add request body to frontend call"
|
||||
))
|
||||
|
||||
stats['matched'] += 1
|
||||
break
|
||||
|
||||
if not matched:
|
||||
issues.append(ContractIssue(
|
||||
severity='ERROR',
|
||||
category='ENDPOINT_NOT_FOUND',
|
||||
message=f"Frontend calls {call.method} {call.endpoint} but no matching backend endpoint found",
|
||||
file_path=call.file_path,
|
||||
line_number=call.line_number,
|
||||
suggestion=f"Create backend endpoint at {call.endpoint} or fix the frontend URL"
|
||||
))
|
||||
stats['unmatched_calls'] += 1
|
||||
|
||||
# Check for unused backend endpoints
|
||||
called_endpoints = set()
|
||||
for call in all_calls:
|
||||
called_endpoints.add((normalize_endpoint(call.endpoint), call.method))
|
||||
|
||||
for ep in all_endpoints:
|
||||
key = (normalize_endpoint(ep.endpoint), ep.method)
|
||||
if key not in called_endpoints:
|
||||
# Check if any call matches with different method
|
||||
matching_calls = [c for c in all_calls
|
||||
if match_endpoints(c.endpoint, ep.endpoint)]
|
||||
if not matching_calls:
|
||||
issues.append(ContractIssue(
|
||||
severity='WARNING',
|
||||
category='UNUSED_ENDPOINT',
|
||||
message=f"Backend endpoint {ep.method} {ep.endpoint} is not called by frontend",
|
||||
file_path=ep.file_path,
|
||||
line_number=None,
|
||||
suggestion="Verify endpoint is needed or remove unused code"
|
||||
))
|
||||
|
||||
return issues, stats
|
||||
|
||||
|
||||
def format_report(issues: list[ContractIssue], stats: dict) -> str:
|
||||
"""Format validation report."""
|
||||
lines = []
|
||||
|
||||
lines.append("")
|
||||
lines.append("=" * 70)
|
||||
lines.append(" API CONTRACT VALIDATION REPORT")
|
||||
lines.append("=" * 70)
|
||||
lines.append("")
|
||||
|
||||
# Stats
|
||||
lines.append("SUMMARY")
|
||||
lines.append("-" * 70)
|
||||
lines.append(f" Frontend API calls found: {stats['frontend_calls']}")
|
||||
lines.append(f" Backend endpoints found: {stats['backend_endpoints']}")
|
||||
lines.append(f" Matched calls: {stats['matched']}")
|
||||
lines.append(f" Unmatched calls: {stats['unmatched_calls']}")
|
||||
lines.append(f" Method mismatches: {stats['method_mismatches']}")
|
||||
lines.append(f" Body mismatches: {stats['body_mismatches']}")
|
||||
lines.append("")
|
||||
|
||||
# Issues by severity
|
||||
errors = [i for i in issues if i.severity == 'ERROR']
|
||||
warnings = [i for i in issues if i.severity == 'WARNING']
|
||||
|
||||
if errors:
|
||||
lines.append("ERRORS (must fix)")
|
||||
lines.append("-" * 70)
|
||||
for i, issue in enumerate(errors, 1):
|
||||
lines.append(f" {i}. [{issue.category}] {issue.message}")
|
||||
lines.append(f" File: {issue.file_path}:{issue.line_number or '?'}")
|
||||
lines.append(f" Fix: {issue.suggestion}")
|
||||
lines.append("")
|
||||
|
||||
if warnings:
|
||||
lines.append("WARNINGS (review)")
|
||||
lines.append("-" * 70)
|
||||
for i, issue in enumerate(warnings, 1):
|
||||
lines.append(f" {i}. [{issue.category}] {issue.message}")
|
||||
lines.append(f" File: {issue.file_path}:{issue.line_number or '?'}")
|
||||
lines.append(f" Fix: {issue.suggestion}")
|
||||
lines.append("")
|
||||
|
||||
# Result
|
||||
lines.append("=" * 70)
|
||||
if not errors:
|
||||
lines.append(" RESULT: PASS (no errors)")
|
||||
else:
|
||||
lines.append(f" RESULT: FAIL ({len(errors)} errors)")
|
||||
lines.append("=" * 70)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Validate API contract")
|
||||
parser.add_argument("--manifest", help="Path to project_manifest.json")
|
||||
parser.add_argument("--project-dir", default=".", help="Project directory")
|
||||
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
||||
parser.add_argument("--strict", action="store_true", help="Fail on warnings too")
|
||||
args = parser.parse_args()
|
||||
|
||||
manifest = None
|
||||
if args.manifest:
|
||||
manifest = load_manifest(args.manifest)
|
||||
|
||||
issues, stats = validate_api_contract(args.project_dir, manifest)
|
||||
|
||||
if args.json:
|
||||
output = {
|
||||
'stats': stats,
|
||||
'issues': [
|
||||
{
|
||||
'severity': i.severity,
|
||||
'category': i.category,
|
||||
'message': i.message,
|
||||
'file_path': i.file_path,
|
||||
'line_number': i.line_number,
|
||||
'suggestion': i.suggestion
|
||||
}
|
||||
for i in issues
|
||||
],
|
||||
'result': 'PASS' if not any(i.severity == 'ERROR' for i in issues) else 'FAIL'
|
||||
}
|
||||
print(json.dumps(output, indent=2))
|
||||
else:
|
||||
print(format_report(issues, stats))
|
||||
|
||||
# Exit code
|
||||
errors = [i for i in issues if i.severity == 'ERROR']
|
||||
warnings = [i for i in issues if i.severity == 'WARNING']
|
||||
|
||||
if errors:
|
||||
return 1
|
||||
if args.strict and warnings:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -0,0 +1,271 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Bash command validator for guardrail enforcement.
|
||||
|
||||
Blocks shell commands that could write files outside the workflow.
|
||||
|
||||
Exit codes:
|
||||
0 = Command allowed
|
||||
1 = Command blocked (with message)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# Patterns that indicate file writing
|
||||
WRITE_PATTERNS = [
|
||||
# Redirections
|
||||
r'\s*>\s*["\']?([^"\'&|;\s]+)', # > file
|
||||
r'\s*>>\s*["\']?([^"\'&|;\s]+)', # >> file
|
||||
r'\s*2>\s*["\']?([^"\'&|;\s]+)', # 2> file
|
||||
r'\s*&>\s*["\']?([^"\'&|;\s]+)', # &> file
|
||||
|
||||
# tee command
|
||||
r'\btee\s+(?:-a\s+)?["\']?([^"\'&|;\s]+)',
|
||||
|
||||
# Direct file creation
|
||||
r'\btouch\s+["\']?([^"\'&|;\s]+)',
|
||||
|
||||
# Copy/Move operations
|
||||
r'\bcp\s+.*\s+["\']?([^"\'&|;\s]+)',
|
||||
r'\bmv\s+.*\s+["\']?([^"\'&|;\s]+)',
|
||||
|
||||
# In-place editing
|
||||
r'\bsed\s+-i',
|
||||
r'\bawk\s+-i\s+inplace',
|
||||
r'\bperl\s+-i',
|
||||
|
||||
# Here documents
|
||||
r'<<\s*["\']?EOF',
|
||||
r'<<\s*["\']?END',
|
||||
r"cat\s*<<",
|
||||
|
||||
# mkdir (could be prep for writing)
|
||||
r'\bmkdir\s+(?:-p\s+)?["\']?([^"\'&|;\s]+)',
|
||||
|
||||
# rm (destructive)
|
||||
r'\brm\s+(?:-rf?\s+)?["\']?([^"\'&|;\s]+)',
|
||||
|
||||
# chmod/chown
|
||||
r'\bchmod\s+',
|
||||
r'\bchown\s+',
|
||||
|
||||
# curl/wget writing to file
|
||||
r'\bcurl\s+.*-o\s+["\']?([^"\'&|;\s]+)',
|
||||
r'\bwget\s+.*-O\s+["\']?([^"\'&|;\s]+)',
|
||||
|
||||
# git operations that modify files
|
||||
r'\bgit\s+checkout\s+',
|
||||
r'\bgit\s+reset\s+--hard',
|
||||
r'\bgit\s+clean\s+',
|
||||
r'\bgit\s+stash\s+pop',
|
||||
|
||||
# npm/yarn install (modifies node_modules)
|
||||
r'\bnpm\s+install\b',
|
||||
r'\byarn\s+add\b',
|
||||
r'\bpnpm\s+add\b',
|
||||
|
||||
# dd command
|
||||
r'\bdd\s+',
|
||||
|
||||
# patch command
|
||||
r'\bpatch\s+',
|
||||
|
||||
# ln (symlinks)
|
||||
r'\bln\s+',
|
||||
]
|
||||
|
||||
# Commands that are always allowed
|
||||
ALWAYS_ALLOWED = [
|
||||
r'^ls\b',
|
||||
r'^cat\s+[^>]+$', # cat without redirect
|
||||
r'^head\b',
|
||||
r'^tail\b',
|
||||
r'^grep\b',
|
||||
r'^find\b',
|
||||
r'^wc\b',
|
||||
r'^echo\s+[^>]+$', # echo without redirect
|
||||
r'^pwd$',
|
||||
r'^cd\b',
|
||||
r'^which\b',
|
||||
r'^type\b',
|
||||
r'^file\b',
|
||||
r'^stat\b',
|
||||
r'^du\b',
|
||||
r'^df\b',
|
||||
r'^ps\b',
|
||||
r'^env$',
|
||||
r'^printenv',
|
||||
r'^date$',
|
||||
r'^whoami$',
|
||||
r'^hostname$',
|
||||
r'^uname\b',
|
||||
r'^git\s+status',
|
||||
r'^git\s+log',
|
||||
r'^git\s+diff',
|
||||
r'^git\s+branch',
|
||||
r'^git\s+show',
|
||||
r'^git\s+remote',
|
||||
r'^npm\s+run\b',
|
||||
r'^npm\s+test\b',
|
||||
r'^npm\s+start\b',
|
||||
r'^npx\b',
|
||||
r'^node\b',
|
||||
r'^python3?\b.*(?!.*>)', # python without redirect
|
||||
r'^pip\s+list',
|
||||
r'^pip\s+show',
|
||||
r'^tree\b',
|
||||
r'^jq\b',
|
||||
r'^curl\s+(?!.*-o)', # curl without -o
|
||||
r'^wget\s+(?!.*-O)', # wget without -O
|
||||
]
|
||||
|
||||
# Paths that are always allowed for writing
|
||||
ALLOWED_PATHS = [
|
||||
'.workflow/',
|
||||
'.claude/',
|
||||
'skills/',
|
||||
'project_manifest.json',
|
||||
'/tmp/',
|
||||
'/var/tmp/',
|
||||
'node_modules/', # npm install
|
||||
'.git/', # git operations
|
||||
]
|
||||
|
||||
|
||||
def is_always_allowed(command: str) -> bool:
|
||||
"""Check if command matches always-allowed patterns."""
|
||||
command = command.strip()
|
||||
for pattern in ALWAYS_ALLOWED:
|
||||
if re.match(pattern, command, re.IGNORECASE):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def extract_target_paths(command: str) -> list:
|
||||
"""Extract potential file paths being written to."""
|
||||
paths = []
|
||||
|
||||
for pattern in WRITE_PATTERNS:
|
||||
matches = re.findall(pattern, command)
|
||||
for match in matches:
|
||||
if isinstance(match, tuple):
|
||||
paths.extend(match)
|
||||
elif match:
|
||||
paths.append(match)
|
||||
|
||||
return [p for p in paths if p and not p.startswith('-')]
|
||||
|
||||
|
||||
def is_path_allowed(path: str) -> bool:
|
||||
"""Check if path is in allowed list."""
|
||||
path = path.lstrip('./')
|
||||
|
||||
for allowed in ALLOWED_PATHS:
|
||||
if path.startswith(allowed) or path == allowed.rstrip('/'):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def has_write_operation(command: str) -> tuple[bool, list]:
|
||||
"""
|
||||
Check if command contains write operations.
|
||||
|
||||
Returns (has_write, target_paths)
|
||||
"""
|
||||
for pattern in WRITE_PATTERNS:
|
||||
if re.search(pattern, command, re.IGNORECASE):
|
||||
paths = extract_target_paths(command)
|
||||
return True, paths
|
||||
|
||||
return False, []
|
||||
|
||||
|
||||
def validate_bash_command(command: str) -> tuple[bool, str]:
|
||||
"""
|
||||
Validate a bash command for guardrail compliance.
|
||||
|
||||
Returns (allowed, message)
|
||||
"""
|
||||
if not command or not command.strip():
|
||||
return True, "✓ GUARDRAIL: Empty command"
|
||||
|
||||
command = command.strip()
|
||||
|
||||
# Check if always allowed
|
||||
if is_always_allowed(command):
|
||||
return True, f"✓ GUARDRAIL: Safe command allowed"
|
||||
|
||||
# Check for write operations
|
||||
has_write, target_paths = has_write_operation(command)
|
||||
|
||||
if not has_write:
|
||||
return True, f"✓ GUARDRAIL: No write operations detected"
|
||||
|
||||
# Check if all target paths are allowed
|
||||
blocked_paths = []
|
||||
for path in target_paths:
|
||||
if not is_path_allowed(path):
|
||||
blocked_paths.append(path)
|
||||
|
||||
if not blocked_paths:
|
||||
return True, f"✓ GUARDRAIL: Write to allowed paths"
|
||||
|
||||
# Block the command
|
||||
suggested_feature = f"modify files via bash"
|
||||
|
||||
error_msg = f"""
|
||||
⛔ GUARDRAIL VIOLATION: Bash command blocked
|
||||
|
||||
Command: {command[:100]}{'...' if len(command) > 100 else ''}
|
||||
|
||||
Detected write operation to unauthorized paths:
|
||||
{chr(10).join(f' - {p}' for p in blocked_paths)}
|
||||
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ 👉 REQUIRED ACTION: Use the workflow instead of bash ║
|
||||
║ ║
|
||||
║ Run this command: ║
|
||||
║ /workflow:spawn {suggested_feature} ║
|
||||
║ ║
|
||||
║ Then use Write/Edit tools (not bash) to modify files. ║
|
||||
║ ║
|
||||
║ Bash is for reading/running, not writing files. ║
|
||||
╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
Allowed bash write targets:
|
||||
- .workflow/*, .claude/*, skills/*
|
||||
- project_manifest.json
|
||||
- /tmp/*, node_modules/
|
||||
"""
|
||||
|
||||
return False, error_msg
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Validate bash command for guardrails")
|
||||
parser.add_argument("--command", help="Bash command to validate")
|
||||
args = parser.parse_args()
|
||||
|
||||
command = args.command or ""
|
||||
|
||||
# Also try reading from stdin if no command provided
|
||||
if not command and not sys.stdin.isatty():
|
||||
command = sys.stdin.read().strip()
|
||||
|
||||
allowed, message = validate_bash_command(command)
|
||||
|
||||
if allowed:
|
||||
print(message)
|
||||
return 0
|
||||
else:
|
||||
print(message, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -0,0 +1,868 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Design Document Validator and Dependency Graph Generator
|
||||
|
||||
Validates design_document.yml and generates:
|
||||
1. dependency_graph.yml - Layered execution order
|
||||
2. Context snapshots for each task
|
||||
3. Tasks with full context
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple
|
||||
|
||||
# Try to import yaml
|
||||
try:
|
||||
import yaml
|
||||
HAS_YAML = True
|
||||
except ImportError:
|
||||
HAS_YAML = False
|
||||
print("Warning: PyYAML not installed. Using basic parser.", file=sys.stderr)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# YAML Helpers
|
||||
# ============================================================================
|
||||
|
||||
def load_yaml(filepath: str) -> dict:
|
||||
"""Load YAML file."""
|
||||
if not os.path.exists(filepath):
|
||||
return {}
|
||||
with open(filepath, 'r') as f:
|
||||
content = f.read()
|
||||
if not content.strip():
|
||||
return {}
|
||||
|
||||
if HAS_YAML:
|
||||
return yaml.safe_load(content) or {}
|
||||
|
||||
# Basic fallback parser (limited)
|
||||
print(f"Warning: Using basic YAML parser for {filepath}", file=sys.stderr)
|
||||
return {}
|
||||
|
||||
|
||||
def save_yaml(filepath: str, data: dict):
|
||||
"""Save data to YAML file."""
|
||||
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||
|
||||
if HAS_YAML:
|
||||
with open(filepath, 'w') as f:
|
||||
yaml.dump(data, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
||||
else:
|
||||
# Simple JSON fallback
|
||||
with open(filepath, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Validation Classes
|
||||
# ============================================================================
|
||||
|
||||
class ValidationError:
|
||||
"""Represents a validation error."""
|
||||
def __init__(self, category: str, entity_id: str, message: str, severity: str = "error"):
|
||||
self.category = category
|
||||
self.entity_id = entity_id
|
||||
self.message = message
|
||||
self.severity = severity # error, warning
|
||||
|
||||
def __str__(self):
|
||||
icon = "❌" if self.severity == "error" else "⚠️"
|
||||
return f"{icon} [{self.category}] {self.entity_id}: {self.message}"
|
||||
|
||||
|
||||
class DesignValidator:
|
||||
"""Validates design document structure and relationships."""
|
||||
|
||||
def __init__(self, design_doc: dict):
|
||||
self.design = design_doc
|
||||
self.errors: List[ValidationError] = []
|
||||
self.warnings: List[ValidationError] = []
|
||||
|
||||
# Collected entity IDs
|
||||
self.model_ids: Set[str] = set()
|
||||
self.api_ids: Set[str] = set()
|
||||
self.page_ids: Set[str] = set()
|
||||
self.component_ids: Set[str] = set()
|
||||
self.all_ids: Set[str] = set()
|
||||
|
||||
def validate(self) -> bool:
|
||||
"""Run all validations. Returns True if no errors."""
|
||||
self._collect_ids()
|
||||
self._validate_models()
|
||||
self._validate_apis()
|
||||
self._validate_pages()
|
||||
self._validate_components()
|
||||
self._validate_no_circular_deps()
|
||||
|
||||
return len(self.errors) == 0
|
||||
|
||||
def _collect_ids(self):
|
||||
"""Collect all entity IDs."""
|
||||
for model in self.design.get('data_models', []):
|
||||
self.model_ids.add(model.get('id', ''))
|
||||
for api in self.design.get('api_endpoints', []):
|
||||
self.api_ids.add(api.get('id', ''))
|
||||
for page in self.design.get('pages', []):
|
||||
self.page_ids.add(page.get('id', ''))
|
||||
for comp in self.design.get('components', []):
|
||||
self.component_ids.add(comp.get('id', ''))
|
||||
|
||||
self.all_ids = self.model_ids | self.api_ids | self.page_ids | self.component_ids
|
||||
|
||||
def _validate_models(self):
|
||||
"""Validate data models."""
|
||||
for model in self.design.get('data_models', []):
|
||||
model_id = model.get('id', 'unknown')
|
||||
|
||||
# Check required fields
|
||||
if not model.get('id'):
|
||||
self.errors.append(ValidationError('model', model_id, "Missing 'id' field"))
|
||||
if not model.get('name'):
|
||||
self.errors.append(ValidationError('model', model_id, "Missing 'name' field"))
|
||||
if not model.get('fields'):
|
||||
self.errors.append(ValidationError('model', model_id, "Missing 'fields' - model has no fields"))
|
||||
|
||||
# Check for primary key
|
||||
fields = model.get('fields', [])
|
||||
has_pk = any('primary_key' in f.get('constraints', []) for f in fields)
|
||||
if not has_pk:
|
||||
self.errors.append(ValidationError('model', model_id, "No primary_key field defined"))
|
||||
|
||||
# Check relations reference existing models
|
||||
for relation in model.get('relations', []):
|
||||
target = relation.get('target', '')
|
||||
if target and target not in self.model_ids:
|
||||
self.errors.append(ValidationError(
|
||||
'model', model_id,
|
||||
f"Relation target '{target}' does not exist"
|
||||
))
|
||||
|
||||
# Check enum fields have values
|
||||
for field in fields:
|
||||
if field.get('type') == 'enum' and not field.get('enum_values'):
|
||||
self.errors.append(ValidationError(
|
||||
'model', model_id,
|
||||
f"Enum field '{field.get('name')}' missing enum_values"
|
||||
))
|
||||
|
||||
def _validate_apis(self):
|
||||
"""Validate API endpoints."""
|
||||
for api in self.design.get('api_endpoints', []):
|
||||
api_id = api.get('id', 'unknown')
|
||||
|
||||
# Check required fields
|
||||
if not api.get('id'):
|
||||
self.errors.append(ValidationError('api', api_id, "Missing 'id' field"))
|
||||
if not api.get('method'):
|
||||
self.errors.append(ValidationError('api', api_id, "Missing 'method' field"))
|
||||
if not api.get('path'):
|
||||
self.errors.append(ValidationError('api', api_id, "Missing 'path' field"))
|
||||
|
||||
# POST/PUT/PATCH should have request_body
|
||||
method = api.get('method', '').upper()
|
||||
if method in ['POST', 'PUT', 'PATCH'] and not api.get('request_body'):
|
||||
self.warnings.append(ValidationError(
|
||||
'api', api_id,
|
||||
f"{method} endpoint should have request_body",
|
||||
severity="warning"
|
||||
))
|
||||
|
||||
# Check at least one response defined
|
||||
if not api.get('responses'):
|
||||
self.errors.append(ValidationError('api', api_id, "No responses defined"))
|
||||
|
||||
# Check model dependencies exist
|
||||
for model_id in api.get('depends_on_models', []):
|
||||
if model_id not in self.model_ids:
|
||||
self.errors.append(ValidationError(
|
||||
'api', api_id,
|
||||
f"depends_on_models references non-existent model '{model_id}'"
|
||||
))
|
||||
|
||||
# Check API dependencies exist
|
||||
for dep_api_id in api.get('depends_on_apis', []):
|
||||
if dep_api_id not in self.api_ids:
|
||||
self.errors.append(ValidationError(
|
||||
'api', api_id,
|
||||
f"depends_on_apis references non-existent API '{dep_api_id}'"
|
||||
))
|
||||
|
||||
def _validate_pages(self):
|
||||
"""Validate pages."""
|
||||
for page in self.design.get('pages', []):
|
||||
page_id = page.get('id', 'unknown')
|
||||
|
||||
# Check required fields
|
||||
if not page.get('id'):
|
||||
self.errors.append(ValidationError('page', page_id, "Missing 'id' field"))
|
||||
if not page.get('path'):
|
||||
self.errors.append(ValidationError('page', page_id, "Missing 'path' field"))
|
||||
|
||||
# Check data_needs reference existing APIs
|
||||
for data_need in page.get('data_needs', []):
|
||||
api_id = data_need.get('api_id', '')
|
||||
if api_id and api_id not in self.api_ids:
|
||||
self.errors.append(ValidationError(
|
||||
'page', page_id,
|
||||
f"data_needs references non-existent API '{api_id}'"
|
||||
))
|
||||
|
||||
# Check components exist
|
||||
for comp_id in page.get('components', []):
|
||||
if comp_id not in self.component_ids:
|
||||
self.errors.append(ValidationError(
|
||||
'page', page_id,
|
||||
f"References non-existent component '{comp_id}'"
|
||||
))
|
||||
|
||||
def _validate_components(self):
|
||||
"""Validate components."""
|
||||
for comp in self.design.get('components', []):
|
||||
comp_id = comp.get('id', 'unknown')
|
||||
|
||||
# Check required fields
|
||||
if not comp.get('id'):
|
||||
self.errors.append(ValidationError('component', comp_id, "Missing 'id' field"))
|
||||
if not comp.get('name'):
|
||||
self.errors.append(ValidationError('component', comp_id, "Missing 'name' field"))
|
||||
|
||||
# Check uses_apis reference existing APIs
|
||||
for api_id in comp.get('uses_apis', []):
|
||||
if api_id not in self.api_ids:
|
||||
self.errors.append(ValidationError(
|
||||
'component', comp_id,
|
||||
f"uses_apis references non-existent API '{api_id}'"
|
||||
))
|
||||
|
||||
# Check uses_components reference existing components
|
||||
for child_id in comp.get('uses_components', []):
|
||||
if child_id not in self.component_ids:
|
||||
self.errors.append(ValidationError(
|
||||
'component', comp_id,
|
||||
f"uses_components references non-existent component '{child_id}'"
|
||||
))
|
||||
|
||||
def _validate_no_circular_deps(self):
|
||||
"""Check for circular dependencies."""
|
||||
# Build dependency graph
|
||||
deps: Dict[str, Set[str]] = defaultdict(set)
|
||||
|
||||
# Model relations
|
||||
for model in self.design.get('data_models', []):
|
||||
model_id = model.get('id', '')
|
||||
for relation in model.get('relations', []):
|
||||
target = relation.get('target', '')
|
||||
if target:
|
||||
deps[model_id].add(target)
|
||||
|
||||
# API dependencies
|
||||
for api in self.design.get('api_endpoints', []):
|
||||
api_id = api.get('id', '')
|
||||
for model_id in api.get('depends_on_models', []):
|
||||
deps[api_id].add(model_id)
|
||||
for dep_api_id in api.get('depends_on_apis', []):
|
||||
deps[api_id].add(dep_api_id)
|
||||
|
||||
# Page dependencies
|
||||
for page in self.design.get('pages', []):
|
||||
page_id = page.get('id', '')
|
||||
for data_need in page.get('data_needs', []):
|
||||
api_id = data_need.get('api_id', '')
|
||||
if api_id:
|
||||
deps[page_id].add(api_id)
|
||||
for comp_id in page.get('components', []):
|
||||
deps[page_id].add(comp_id)
|
||||
|
||||
# Component dependencies
|
||||
for comp in self.design.get('components', []):
|
||||
comp_id = comp.get('id', '')
|
||||
for api_id in comp.get('uses_apis', []):
|
||||
deps[comp_id].add(api_id)
|
||||
for child_id in comp.get('uses_components', []):
|
||||
deps[comp_id].add(child_id)
|
||||
|
||||
# Detect cycles using DFS
|
||||
visited = set()
|
||||
rec_stack = set()
|
||||
|
||||
def has_cycle(node: str, path: List[str]) -> Optional[List[str]]:
|
||||
visited.add(node)
|
||||
rec_stack.add(node)
|
||||
path.append(node)
|
||||
|
||||
for neighbor in deps.get(node, []):
|
||||
if neighbor not in visited:
|
||||
result = has_cycle(neighbor, path)
|
||||
if result:
|
||||
return result
|
||||
elif neighbor in rec_stack:
|
||||
# Found cycle
|
||||
cycle_start = path.index(neighbor)
|
||||
return path[cycle_start:] + [neighbor]
|
||||
|
||||
path.pop()
|
||||
rec_stack.remove(node)
|
||||
return None
|
||||
|
||||
for entity_id in self.all_ids:
|
||||
if entity_id not in visited:
|
||||
cycle = has_cycle(entity_id, [])
|
||||
if cycle:
|
||||
self.errors.append(ValidationError(
|
||||
'dependency', entity_id,
|
||||
f"Circular dependency detected: {' → '.join(cycle)}"
|
||||
))
|
||||
|
||||
def print_report(self):
|
||||
"""Print validation report."""
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("DESIGN VALIDATION REPORT".center(60))
|
||||
print("=" * 60)
|
||||
|
||||
# Summary
|
||||
print()
|
||||
print(f" Models: {len(self.model_ids)}")
|
||||
print(f" APIs: {len(self.api_ids)}")
|
||||
print(f" Pages: {len(self.page_ids)}")
|
||||
print(f" Components: {len(self.component_ids)}")
|
||||
print(f" Total: {len(self.all_ids)}")
|
||||
|
||||
# Errors
|
||||
if self.errors:
|
||||
print()
|
||||
print("-" * 60)
|
||||
print(f"ERRORS ({len(self.errors)})")
|
||||
print("-" * 60)
|
||||
for error in self.errors:
|
||||
print(f" {error}")
|
||||
|
||||
# Warnings
|
||||
if self.warnings:
|
||||
print()
|
||||
print("-" * 60)
|
||||
print(f"WARNINGS ({len(self.warnings)})")
|
||||
print("-" * 60)
|
||||
for warning in self.warnings:
|
||||
print(f" {warning}")
|
||||
|
||||
# Result
|
||||
print()
|
||||
print("=" * 60)
|
||||
if self.errors:
|
||||
print("❌ VALIDATION FAILED".center(60))
|
||||
else:
|
||||
print("✅ VALIDATION PASSED".center(60))
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Dependency Graph Generator
|
||||
# ============================================================================
|
||||
|
||||
class DependencyGraphGenerator:
|
||||
"""Generates dependency graph and execution layers from design document."""
|
||||
|
||||
def __init__(self, design_doc: dict):
|
||||
self.design = design_doc
|
||||
self.deps: Dict[str, Set[str]] = defaultdict(set)
|
||||
self.reverse_deps: Dict[str, Set[str]] = defaultdict(set)
|
||||
self.entity_types: Dict[str, str] = {}
|
||||
self.entity_names: Dict[str, str] = {}
|
||||
self.layers: List[List[str]] = []
|
||||
|
||||
def generate(self) -> dict:
|
||||
"""Generate the full dependency graph."""
|
||||
self._build_dependency_map()
|
||||
self._calculate_layers()
|
||||
return self._build_graph_document()
|
||||
|
||||
def _build_dependency_map(self):
|
||||
"""Build forward and reverse dependency maps."""
|
||||
# Models
|
||||
for model in self.design.get('data_models', []):
|
||||
model_id = model.get('id', '')
|
||||
self.entity_types[model_id] = 'model'
|
||||
self.entity_names[model_id] = model.get('name', model_id)
|
||||
|
||||
for relation in model.get('relations', []):
|
||||
target = relation.get('target', '')
|
||||
if target:
|
||||
self.deps[model_id].add(target)
|
||||
self.reverse_deps[target].add(model_id)
|
||||
|
||||
# APIs
|
||||
for api in self.design.get('api_endpoints', []):
|
||||
api_id = api.get('id', '')
|
||||
self.entity_types[api_id] = 'api'
|
||||
self.entity_names[api_id] = api.get('summary', api_id)
|
||||
|
||||
for model_id in api.get('depends_on_models', []):
|
||||
self.deps[api_id].add(model_id)
|
||||
self.reverse_deps[model_id].add(api_id)
|
||||
|
||||
for dep_api_id in api.get('depends_on_apis', []):
|
||||
self.deps[api_id].add(dep_api_id)
|
||||
self.reverse_deps[dep_api_id].add(api_id)
|
||||
|
||||
# Pages
|
||||
for page in self.design.get('pages', []):
|
||||
page_id = page.get('id', '')
|
||||
self.entity_types[page_id] = 'page'
|
||||
self.entity_names[page_id] = page.get('name', page_id)
|
||||
|
||||
for data_need in page.get('data_needs', []):
|
||||
api_id = data_need.get('api_id', '')
|
||||
if api_id:
|
||||
self.deps[page_id].add(api_id)
|
||||
self.reverse_deps[api_id].add(page_id)
|
||||
|
||||
for comp_id in page.get('components', []):
|
||||
self.deps[page_id].add(comp_id)
|
||||
self.reverse_deps[comp_id].add(page_id)
|
||||
|
||||
# Components
|
||||
for comp in self.design.get('components', []):
|
||||
comp_id = comp.get('id', '')
|
||||
self.entity_types[comp_id] = 'component'
|
||||
self.entity_names[comp_id] = comp.get('name', comp_id)
|
||||
|
||||
for api_id in comp.get('uses_apis', []):
|
||||
self.deps[comp_id].add(api_id)
|
||||
self.reverse_deps[api_id].add(comp_id)
|
||||
|
||||
for child_id in comp.get('uses_components', []):
|
||||
self.deps[comp_id].add(child_id)
|
||||
self.reverse_deps[child_id].add(comp_id)
|
||||
|
||||
def _calculate_layers(self):
|
||||
"""Calculate execution layers using topological sort."""
|
||||
# Find all entities with no dependencies (Layer 1)
|
||||
all_entities = set(self.entity_types.keys())
|
||||
remaining = all_entities.copy()
|
||||
assigned = set()
|
||||
|
||||
while remaining:
|
||||
# Find entities whose dependencies are all assigned
|
||||
layer = []
|
||||
for entity_id in remaining:
|
||||
deps = self.deps.get(entity_id, set())
|
||||
if deps.issubset(assigned):
|
||||
layer.append(entity_id)
|
||||
|
||||
if not layer:
|
||||
# Shouldn't happen if no circular deps, but safety check
|
||||
print(f"Warning: Could not assign remaining entities: {remaining}", file=sys.stderr)
|
||||
break
|
||||
|
||||
self.layers.append(sorted(layer))
|
||||
for entity_id in layer:
|
||||
remaining.remove(entity_id)
|
||||
assigned.add(entity_id)
|
||||
|
||||
def _build_graph_document(self) -> dict:
|
||||
"""Build the dependency graph document."""
|
||||
# Calculate stats
|
||||
max_parallelism = max(len(layer) for layer in self.layers) if self.layers else 0
|
||||
critical_path = len(self.layers)
|
||||
|
||||
graph = {
|
||||
'dependency_graph': {
|
||||
'design_version': self.design.get('revision', 1),
|
||||
'workflow_version': self.design.get('workflow_version', 'v001'),
|
||||
'generated_at': datetime.now().isoformat(),
|
||||
'generator': 'validate_design.py',
|
||||
'stats': {
|
||||
'total_entities': len(self.entity_types),
|
||||
'total_layers': len(self.layers),
|
||||
'max_parallelism': max_parallelism,
|
||||
'critical_path_length': critical_path
|
||||
}
|
||||
},
|
||||
'layers': [],
|
||||
'dependency_map': {},
|
||||
'task_map': []
|
||||
}
|
||||
|
||||
# Build layers
|
||||
layer_names = {
|
||||
1: ("Data Layer", "Database models - no external dependencies"),
|
||||
2: ("API Layer", "REST endpoints - depend on models"),
|
||||
3: ("UI Layer", "Pages and components - depend on APIs"),
|
||||
}
|
||||
|
||||
for i, layer_entities in enumerate(self.layers, 1):
|
||||
name, desc = layer_names.get(i, (f"Layer {i}", f"Entities with {i-1} levels of dependencies"))
|
||||
|
||||
layer_items = []
|
||||
for entity_id in layer_entities:
|
||||
entity_type = self.entity_types.get(entity_id, 'unknown')
|
||||
agent = 'backend' if entity_type in ['model', 'api'] else 'frontend'
|
||||
|
||||
layer_items.append({
|
||||
'id': entity_id,
|
||||
'type': entity_type,
|
||||
'name': self.entity_names.get(entity_id, entity_id),
|
||||
'depends_on': list(self.deps.get(entity_id, [])),
|
||||
'task_id': f"task_create_{entity_id}",
|
||||
'agent': agent,
|
||||
'complexity': 'medium' # Could be calculated
|
||||
})
|
||||
|
||||
graph['layers'].append({
|
||||
'layer': i,
|
||||
'name': name,
|
||||
'description': desc,
|
||||
'items': layer_items,
|
||||
'requires_layers': list(range(1, i)) if i > 1 else [],
|
||||
'parallel_count': len(layer_items)
|
||||
})
|
||||
|
||||
# Build dependency map
|
||||
for entity_id in self.entity_types:
|
||||
graph['dependency_map'][entity_id] = {
|
||||
'type': self.entity_types.get(entity_id),
|
||||
'layer': self._get_layer_number(entity_id),
|
||||
'depends_on': list(self.deps.get(entity_id, [])),
|
||||
'depended_by': list(self.reverse_deps.get(entity_id, []))
|
||||
}
|
||||
|
||||
return graph
|
||||
|
||||
def _get_layer_number(self, entity_id: str) -> int:
|
||||
"""Get the layer number for an entity."""
|
||||
for i, layer in enumerate(self.layers, 1):
|
||||
if entity_id in layer:
|
||||
return i
|
||||
return 0
|
||||
|
||||
def print_layers(self):
|
||||
"""Print layer visualization."""
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("EXECUTION LAYERS".center(60))
|
||||
print("=" * 60)
|
||||
|
||||
for i, layer_entities in enumerate(self.layers, 1):
|
||||
print()
|
||||
print(f"Layer {i}: ({len(layer_entities)} items - parallel)")
|
||||
print("-" * 40)
|
||||
|
||||
for entity_id in layer_entities:
|
||||
entity_type = self.entity_types.get(entity_id, '?')
|
||||
icon = {'model': '📦', 'api': '🔌', 'page': '📄', 'component': '🧩'}.get(entity_type, '❓')
|
||||
deps = self.deps.get(entity_id, set())
|
||||
deps_str = f" ← [{', '.join(deps)}]" if deps else ""
|
||||
print(f" {icon} {entity_id}{deps_str}")
|
||||
|
||||
print()
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Context Generator
|
||||
# ============================================================================
|
||||
|
||||
class ContextGenerator:
|
||||
"""Generates context snapshots for tasks."""
|
||||
|
||||
def __init__(self, design_doc: dict, graph: dict, output_dir: str):
|
||||
self.design = design_doc
|
||||
self.graph = graph
|
||||
self.output_dir = output_dir
|
||||
|
||||
# Index design entities by ID for quick lookup
|
||||
self.models: Dict[str, dict] = {}
|
||||
self.apis: Dict[str, dict] = {}
|
||||
self.pages: Dict[str, dict] = {}
|
||||
self.components: Dict[str, dict] = {}
|
||||
|
||||
self._index_entities()
|
||||
|
||||
def _index_entities(self):
|
||||
"""Index all entities by ID."""
|
||||
for model in self.design.get('data_models', []):
|
||||
self.models[model.get('id', '')] = model
|
||||
for api in self.design.get('api_endpoints', []):
|
||||
self.apis[api.get('id', '')] = api
|
||||
for page in self.design.get('pages', []):
|
||||
self.pages[page.get('id', '')] = page
|
||||
for comp in self.design.get('components', []):
|
||||
self.components[comp.get('id', '')] = comp
|
||||
|
||||
def generate_all_contexts(self):
|
||||
"""Generate context files for all entities."""
|
||||
contexts_dir = Path(self.output_dir) / 'contexts'
|
||||
contexts_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for entity_id, entity_info in self.graph.get('dependency_map', {}).items():
|
||||
context = self._generate_context(entity_id, entity_info)
|
||||
context_path = contexts_dir / f"{entity_id}.yml"
|
||||
save_yaml(str(context_path), context)
|
||||
|
||||
print(f"Generated {len(self.graph.get('dependency_map', {}))} context files in {contexts_dir}")
|
||||
|
||||
def _generate_context(self, entity_id: str, entity_info: dict) -> dict:
|
||||
"""Generate context for a single entity."""
|
||||
entity_type = entity_info.get('type', '')
|
||||
deps = entity_info.get('depends_on', [])
|
||||
|
||||
context = {
|
||||
'task_id': f"task_create_{entity_id}",
|
||||
'entity_id': entity_id,
|
||||
'generated_at': datetime.now().isoformat(),
|
||||
'workflow_version': self.graph.get('dependency_graph', {}).get('workflow_version', 'v001'),
|
||||
'target': {
|
||||
'type': entity_type,
|
||||
'definition': self._get_entity_definition(entity_id, entity_type)
|
||||
},
|
||||
'related': {
|
||||
'models': [],
|
||||
'apis': [],
|
||||
'components': []
|
||||
},
|
||||
'dependencies': {
|
||||
'entity_ids': deps,
|
||||
'definitions': []
|
||||
},
|
||||
'files': {
|
||||
'to_create': self._get_files_to_create(entity_id, entity_type),
|
||||
'reference': []
|
||||
},
|
||||
'acceptance': self._get_acceptance_criteria(entity_id, entity_type)
|
||||
}
|
||||
|
||||
# Add related entity definitions
|
||||
for dep_id in deps:
|
||||
dep_info = self.graph.get('dependency_map', {}).get(dep_id, {})
|
||||
dep_type = dep_info.get('type', '')
|
||||
dep_def = self._get_entity_definition(dep_id, dep_type)
|
||||
|
||||
if dep_type == 'model':
|
||||
context['related']['models'].append({'id': dep_id, 'definition': dep_def})
|
||||
elif dep_type == 'api':
|
||||
context['related']['apis'].append({'id': dep_id, 'definition': dep_def})
|
||||
elif dep_type == 'component':
|
||||
context['related']['components'].append({'id': dep_id, 'definition': dep_def})
|
||||
|
||||
context['dependencies']['definitions'].append({
|
||||
'id': dep_id,
|
||||
'type': dep_type,
|
||||
'definition': dep_def
|
||||
})
|
||||
|
||||
return context
|
||||
|
||||
def _get_entity_definition(self, entity_id: str, entity_type: str) -> dict:
|
||||
"""Get the full definition for an entity."""
|
||||
if entity_type == 'model':
|
||||
return self.models.get(entity_id, {})
|
||||
elif entity_type == 'api':
|
||||
return self.apis.get(entity_id, {})
|
||||
elif entity_type == 'page':
|
||||
return self.pages.get(entity_id, {})
|
||||
elif entity_type == 'component':
|
||||
return self.components.get(entity_id, {})
|
||||
return {}
|
||||
|
||||
def _get_files_to_create(self, entity_id: str, entity_type: str) -> List[str]:
|
||||
"""Get list of files to create for an entity."""
|
||||
if entity_type == 'model':
|
||||
name = self.models.get(entity_id, {}).get('name', entity_id)
|
||||
return [
|
||||
'prisma/schema.prisma',
|
||||
f'app/models/{name.lower()}.ts'
|
||||
]
|
||||
elif entity_type == 'api':
|
||||
path = self.apis.get(entity_id, {}).get('path', '/api/unknown')
|
||||
route_path = path.replace('/api/', '').replace(':', '')
|
||||
return [f'app/api/{route_path}/route.ts']
|
||||
elif entity_type == 'page':
|
||||
path = self.pages.get(entity_id, {}).get('path', '/unknown')
|
||||
return [f'app{path}/page.tsx']
|
||||
elif entity_type == 'component':
|
||||
name = self.components.get(entity_id, {}).get('name', 'Unknown')
|
||||
return [f'app/components/{name}.tsx']
|
||||
return []
|
||||
|
||||
def _get_acceptance_criteria(self, entity_id: str, entity_type: str) -> List[dict]:
|
||||
"""Get acceptance criteria for an entity."""
|
||||
criteria = []
|
||||
|
||||
if entity_type == 'model':
|
||||
criteria = [
|
||||
{'criterion': 'Model defined in Prisma schema', 'verification': 'Check prisma/schema.prisma'},
|
||||
{'criterion': 'TypeScript types exported', 'verification': 'Import type in test file'},
|
||||
{'criterion': 'Relations properly configured', 'verification': 'Check Prisma relations'},
|
||||
]
|
||||
elif entity_type == 'api':
|
||||
api = self.apis.get(entity_id, {})
|
||||
method = api.get('method', 'GET')
|
||||
path = api.get('path', '/api/unknown')
|
||||
criteria = [
|
||||
{'criterion': f'{method} {path} returns success response', 'verification': f'curl -X {method} {path}'},
|
||||
{'criterion': 'Request validation implemented', 'verification': 'Test with invalid data'},
|
||||
{'criterion': 'Error responses match contract', 'verification': 'Test error scenarios'},
|
||||
]
|
||||
elif entity_type == 'page':
|
||||
page = self.pages.get(entity_id, {})
|
||||
path = page.get('path', '/unknown')
|
||||
criteria = [
|
||||
{'criterion': f'Page renders at {path}', 'verification': f'Navigate to {path}'},
|
||||
{'criterion': 'Data fetching works', 'verification': 'Check network tab'},
|
||||
{'criterion': 'Components render correctly', 'verification': 'Visual inspection'},
|
||||
]
|
||||
elif entity_type == 'component':
|
||||
criteria = [
|
||||
{'criterion': 'Component renders without errors', 'verification': 'Import and render in test'},
|
||||
{'criterion': 'Props are typed correctly', 'verification': 'TypeScript compilation'},
|
||||
{'criterion': 'Events fire correctly', 'verification': 'Test event handlers'},
|
||||
]
|
||||
|
||||
return criteria
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Task Generator
|
||||
# ============================================================================
|
||||
|
||||
class TaskGenerator:
|
||||
"""Generates task files with full context."""
|
||||
|
||||
def __init__(self, design_doc: dict, graph: dict, output_dir: str):
|
||||
self.design = design_doc
|
||||
self.graph = graph
|
||||
self.output_dir = output_dir
|
||||
|
||||
def generate_all_tasks(self):
|
||||
"""Generate task files for all entities."""
|
||||
tasks_dir = Path(self.output_dir) / 'tasks'
|
||||
tasks_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
task_count = 0
|
||||
for layer in self.graph.get('layers', []):
|
||||
for item in layer.get('items', []):
|
||||
task = self._generate_task(item, layer.get('layer', 1))
|
||||
task_path = tasks_dir / f"{task['id']}.yml"
|
||||
save_yaml(str(task_path), task)
|
||||
task_count += 1
|
||||
|
||||
print(f"Generated {task_count} task files in {tasks_dir}")
|
||||
|
||||
def _generate_task(self, item: dict, layer_num: int) -> dict:
|
||||
"""Generate a task for an entity."""
|
||||
entity_id = item.get('id', '')
|
||||
entity_type = item.get('type', '')
|
||||
|
||||
task = {
|
||||
'id': item.get('task_id', f'task_create_{entity_id}'),
|
||||
'type': 'create',
|
||||
'title': f"Create {item.get('name', entity_id)}",
|
||||
'agent': item.get('agent', 'backend'),
|
||||
'entity_id': entity_id,
|
||||
'entity_ids': [entity_id],
|
||||
'status': 'pending',
|
||||
'layer': layer_num,
|
||||
'parallel_group': f"layer_{layer_num}",
|
||||
'complexity': item.get('complexity', 'medium'),
|
||||
'dependencies': [f"task_create_{dep}" for dep in item.get('depends_on', [])],
|
||||
'context': {
|
||||
'design_version': self.graph.get('dependency_graph', {}).get('design_version', 1),
|
||||
'workflow_version': self.graph.get('dependency_graph', {}).get('workflow_version', 'v001'),
|
||||
'context_snapshot_path': f".workflow/versions/v001/contexts/{entity_id}.yml"
|
||||
},
|
||||
'created_at': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
return task
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main CLI
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Validate design document and generate dependency graph")
|
||||
parser.add_argument('design_file', help='Path to design_document.yml')
|
||||
parser.add_argument('--output-dir', '-o', default='.workflow/versions/v001',
|
||||
help='Output directory for generated files')
|
||||
parser.add_argument('--validate-only', '-v', action='store_true',
|
||||
help='Only validate, do not generate files')
|
||||
parser.add_argument('--quiet', '-q', action='store_true',
|
||||
help='Suppress output except errors')
|
||||
parser.add_argument('--json', action='store_true',
|
||||
help='Output validation result as JSON')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load design document
|
||||
design = load_yaml(args.design_file)
|
||||
if not design:
|
||||
print(f"Error: Could not load design document: {args.design_file}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Validate
|
||||
validator = DesignValidator(design)
|
||||
is_valid = validator.validate()
|
||||
|
||||
if args.json:
|
||||
result = {
|
||||
'valid': is_valid,
|
||||
'errors': [str(e) for e in validator.errors],
|
||||
'warnings': [str(w) for w in validator.warnings],
|
||||
'stats': {
|
||||
'models': len(validator.model_ids),
|
||||
'apis': len(validator.api_ids),
|
||||
'pages': len(validator.page_ids),
|
||||
'components': len(validator.component_ids)
|
||||
}
|
||||
}
|
||||
print(json.dumps(result, indent=2))
|
||||
sys.exit(0 if is_valid else 1)
|
||||
|
||||
if not args.quiet:
|
||||
validator.print_report()
|
||||
|
||||
if not is_valid:
|
||||
sys.exit(1)
|
||||
|
||||
if args.validate_only:
|
||||
sys.exit(0)
|
||||
|
||||
# Generate dependency graph
|
||||
generator = DependencyGraphGenerator(design)
|
||||
graph = generator.generate()
|
||||
|
||||
if not args.quiet:
|
||||
generator.print_layers()
|
||||
|
||||
# Save dependency graph
|
||||
output_dir = Path(args.output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
graph_path = output_dir / 'dependency_graph.yml'
|
||||
save_yaml(str(graph_path), graph)
|
||||
print(f"Saved dependency graph to: {graph_path}")
|
||||
|
||||
# Generate context files
|
||||
context_gen = ContextGenerator(design, graph, str(output_dir))
|
||||
context_gen.generate_all_contexts()
|
||||
|
||||
# Generate task files
|
||||
task_gen = TaskGenerator(design, graph, str(output_dir))
|
||||
task_gen.generate_all_tasks()
|
||||
|
||||
print()
|
||||
print("✅ Design validation and generation complete!")
|
||||
print(f" Output directory: {output_dir}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Validate project manifest integrity."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def validate_structure(manifest: dict) -> list:
|
||||
"""Validate manifest has required structure."""
|
||||
errors = []
|
||||
required_keys = ["project", "state", "entities", "dependencies"]
|
||||
|
||||
for key in required_keys:
|
||||
if key not in manifest:
|
||||
errors.append(f"Missing required key: {key}")
|
||||
|
||||
if "entities" in manifest:
|
||||
entity_types = ["pages", "components", "api_endpoints", "database_tables"]
|
||||
for etype in entity_types:
|
||||
if etype not in manifest["entities"]:
|
||||
errors.append(f"Missing entity type: {etype}")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def validate_pages(pages: list) -> list:
|
||||
"""Validate page entities."""
|
||||
errors = []
|
||||
for page in pages:
|
||||
if "id" not in page:
|
||||
errors.append(f"Page missing id: {page}")
|
||||
if "path" not in page:
|
||||
errors.append(f"Page {page.get('id', 'unknown')} missing path")
|
||||
if "file_path" not in page:
|
||||
errors.append(f"Page {page.get('id', 'unknown')} missing file_path")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_components(components: list) -> list:
|
||||
"""Validate component entities."""
|
||||
errors = []
|
||||
for comp in components:
|
||||
if "id" not in comp:
|
||||
errors.append(f"Component missing id: {comp}")
|
||||
if "name" not in comp:
|
||||
errors.append(f"Component {comp.get('id', 'unknown')} missing name")
|
||||
if "file_path" not in comp:
|
||||
errors.append(f"Component {comp.get('id', 'unknown')} missing file_path")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_apis(apis: list) -> list:
|
||||
"""Validate API endpoint entities."""
|
||||
errors = []
|
||||
for api in apis:
|
||||
if "id" not in api:
|
||||
errors.append(f"API missing id: {api}")
|
||||
if "method" not in api:
|
||||
errors.append(f"API {api.get('id', 'unknown')} missing method")
|
||||
if "path" not in api:
|
||||
errors.append(f"API {api.get('id', 'unknown')} missing path")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_tables(tables: list) -> list:
|
||||
"""Validate database table entities."""
|
||||
errors = []
|
||||
for table in tables:
|
||||
if "id" not in table:
|
||||
errors.append(f"Table missing id: {table}")
|
||||
if "name" not in table:
|
||||
errors.append(f"Table {table.get('id', 'unknown')} missing name")
|
||||
if "columns" not in table:
|
||||
errors.append(f"Table {table.get('id', 'unknown')} missing columns")
|
||||
return errors
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Validate project manifest")
|
||||
parser.add_argument("--strict", action="store_true", help="Treat warnings as errors")
|
||||
parser.add_argument("--manifest", default="project_manifest.json", help="Path to manifest")
|
||||
args = parser.parse_args()
|
||||
|
||||
manifest_path = args.manifest
|
||||
if not os.path.isabs(manifest_path):
|
||||
manifest_path = os.path.join(os.getcwd(), manifest_path)
|
||||
|
||||
if not os.path.exists(manifest_path):
|
||||
print(f"Error: Manifest not found at {manifest_path}")
|
||||
return 1
|
||||
|
||||
with open(manifest_path) as f:
|
||||
manifest = json.load(f)
|
||||
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
# Structure validation
|
||||
errors.extend(validate_structure(manifest))
|
||||
|
||||
if "entities" in manifest:
|
||||
errors.extend(validate_pages(manifest["entities"].get("pages", [])))
|
||||
errors.extend(validate_components(manifest["entities"].get("components", [])))
|
||||
errors.extend(validate_apis(manifest["entities"].get("api_endpoints", [])))
|
||||
errors.extend(validate_tables(manifest["entities"].get("database_tables", [])))
|
||||
|
||||
# Report results
|
||||
if errors:
|
||||
print("VALIDATION FAILED")
|
||||
for error in errors:
|
||||
print(f" ERROR: {error}")
|
||||
return 1
|
||||
|
||||
if warnings:
|
||||
print("VALIDATION PASSED WITH WARNINGS")
|
||||
for warning in warnings:
|
||||
print(f" WARNING: {warning}")
|
||||
if args.strict:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
print("VALIDATION PASSED")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
|
|
@ -0,0 +1,478 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Workflow enforcement hook for Claude Code.
|
||||
Validates that operations comply with current workflow phase.
|
||||
|
||||
When blocked, instructs AI to run /workflow:spawn to start a proper workflow.
|
||||
|
||||
Exit codes:
|
||||
0 = Operation allowed
|
||||
1 = Operation blocked (with message)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Try to import yaml
|
||||
try:
|
||||
import yaml
|
||||
HAS_YAML = True
|
||||
except ImportError:
|
||||
HAS_YAML = False
|
||||
|
||||
|
||||
def load_yaml(filepath: str) -> dict:
|
||||
"""Load YAML file."""
|
||||
if not os.path.exists(filepath):
|
||||
return {}
|
||||
|
||||
with open(filepath, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
if not content.strip():
|
||||
return {}
|
||||
|
||||
if HAS_YAML:
|
||||
return yaml.safe_load(content) or {}
|
||||
|
||||
# Simple fallback parser
|
||||
result = {}
|
||||
current_list = None
|
||||
for line in content.split('\n'):
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith('#'):
|
||||
continue
|
||||
# Handle list items
|
||||
if stripped.startswith('- '):
|
||||
if current_list is not None:
|
||||
value = stripped[2:].strip()
|
||||
if (value.startswith('"') and value.endswith('"')) or \
|
||||
(value.startswith("'") and value.endswith("'")):
|
||||
value = value[1:-1]
|
||||
current_list.append(value)
|
||||
continue
|
||||
if ':' in stripped:
|
||||
key, _, value = stripped.partition(':')
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
if value == '' or value == '[]':
|
||||
result[key] = []
|
||||
current_list = result[key]
|
||||
elif value == 'null' or value == '~':
|
||||
result[key] = None
|
||||
current_list = None
|
||||
elif value == 'true':
|
||||
result[key] = True
|
||||
current_list = None
|
||||
elif value == 'false':
|
||||
result[key] = False
|
||||
current_list = None
|
||||
elif value.isdigit():
|
||||
result[key] = int(value)
|
||||
current_list = None
|
||||
else:
|
||||
if (value.startswith('"') and value.endswith('"')) or \
|
||||
(value.startswith("'") and value.endswith("'")):
|
||||
value = value[1:-1]
|
||||
result[key] = value
|
||||
current_list = None
|
||||
return result
|
||||
|
||||
|
||||
def get_current_phase() -> str:
|
||||
"""Get current workflow phase from version session."""
|
||||
workflow_dir = Path('.workflow')
|
||||
current_path = workflow_dir / 'current.yml'
|
||||
|
||||
if not current_path.exists():
|
||||
return 'NO_WORKFLOW'
|
||||
|
||||
current = load_yaml(str(current_path))
|
||||
active_version = current.get('active_version')
|
||||
|
||||
if not active_version:
|
||||
return 'NO_WORKFLOW'
|
||||
|
||||
session_path = workflow_dir / 'versions' / active_version / 'session.yml'
|
||||
if not session_path.exists():
|
||||
return 'NO_WORKFLOW'
|
||||
|
||||
session = load_yaml(str(session_path))
|
||||
return session.get('current_phase', 'UNKNOWN')
|
||||
|
||||
|
||||
def get_active_version() -> str:
|
||||
"""Get active workflow version."""
|
||||
workflow_dir = Path('.workflow')
|
||||
current_path = workflow_dir / 'current.yml'
|
||||
|
||||
if not current_path.exists():
|
||||
return None
|
||||
|
||||
current = load_yaml(str(current_path))
|
||||
return current.get('active_version')
|
||||
|
||||
|
||||
def get_workflow_feature() -> str:
|
||||
"""Get the feature name from current workflow."""
|
||||
workflow_dir = Path('.workflow')
|
||||
current_path = workflow_dir / 'current.yml'
|
||||
|
||||
if not current_path.exists():
|
||||
return None
|
||||
|
||||
current = load_yaml(str(current_path))
|
||||
active_version = current.get('active_version')
|
||||
|
||||
if not active_version:
|
||||
return None
|
||||
|
||||
session_path = workflow_dir / 'versions' / active_version / 'session.yml'
|
||||
if not session_path.exists():
|
||||
return None
|
||||
|
||||
session = load_yaml(str(session_path))
|
||||
return session.get('feature', 'unknown feature')
|
||||
|
||||
|
||||
def count_task_files(version: str) -> int:
|
||||
"""Count task files in version directory."""
|
||||
tasks_dir = Path('.workflow') / 'versions' / version / 'tasks'
|
||||
if not tasks_dir.exists():
|
||||
return 0
|
||||
return len(list(tasks_dir.glob('task_*.yml')))
|
||||
|
||||
|
||||
def extract_feature_from_file(file_path: str) -> str:
|
||||
"""Extract a feature description from the file path."""
|
||||
# Convert path to a human-readable feature description
|
||||
parts = Path(file_path).parts
|
||||
|
||||
# Remove common prefixes
|
||||
skip = {'src', 'app', 'lib', 'components', 'pages', 'api', 'utils', 'hooks'}
|
||||
meaningful = [p for p in parts if p not in skip and not p.startswith('.')]
|
||||
|
||||
if meaningful:
|
||||
# Get the file name without extension
|
||||
name = Path(file_path).stem
|
||||
return f"update {name}"
|
||||
|
||||
return f"modify {file_path}"
|
||||
|
||||
|
||||
def validate_task_spawn(tool_input: dict) -> tuple[bool, str]:
|
||||
"""
|
||||
Validate Task tool spawning for workflow compliance.
|
||||
"""
|
||||
phase = get_current_phase()
|
||||
prompt = tool_input.get('prompt', '')
|
||||
subagent_type = tool_input.get('subagent_type', '')
|
||||
|
||||
agent_type = subagent_type.lower()
|
||||
|
||||
# Check architect agent
|
||||
if 'system-architect' in agent_type or 'ARCHITECT AGENT' in prompt.upper():
|
||||
if phase not in ['DESIGNING', 'NO_WORKFLOW']:
|
||||
return False, f"""
|
||||
⛔ WORKFLOW VIOLATION: Cannot spawn Architect agent
|
||||
|
||||
Current Phase: {phase}
|
||||
Required Phase: DESIGNING
|
||||
|
||||
The Architect agent can only be spawned during the DESIGNING phase.
|
||||
|
||||
👉 REQUIRED ACTION: Run /workflow:status to check current state.
|
||||
"""
|
||||
|
||||
# Check frontend agent
|
||||
if 'frontend' in agent_type or 'FRONTEND AGENT' in prompt.upper():
|
||||
if phase not in ['IMPLEMENTING', 'IMPL_REJECTED']:
|
||||
return False, f"""
|
||||
⛔ WORKFLOW VIOLATION: Cannot spawn Frontend agent
|
||||
|
||||
Current Phase: {phase}
|
||||
Required Phase: IMPLEMENTING
|
||||
|
||||
👉 REQUIRED ACTION: Complete the design phase first, then run /workflow:approve
|
||||
"""
|
||||
|
||||
version = get_active_version()
|
||||
if version and count_task_files(version) == 0:
|
||||
return False, f"""
|
||||
⛔ WORKFLOW VIOLATION: No task files found
|
||||
|
||||
Cannot start implementation without design tasks.
|
||||
|
||||
👉 REQUIRED ACTION: Ensure Architect agent created task files in:
|
||||
.workflow/versions/{version}/tasks/
|
||||
"""
|
||||
|
||||
# Check backend agent
|
||||
if 'backend' in agent_type or 'BACKEND AGENT' in prompt.upper():
|
||||
if phase not in ['IMPLEMENTING', 'IMPL_REJECTED']:
|
||||
return False, f"""
|
||||
⛔ WORKFLOW VIOLATION: Cannot spawn Backend agent
|
||||
|
||||
Current Phase: {phase}
|
||||
Required Phase: IMPLEMENTING
|
||||
|
||||
👉 REQUIRED ACTION: Complete the design phase first, then run /workflow:approve
|
||||
"""
|
||||
|
||||
# Check reviewer agent
|
||||
if 'quality' in agent_type or 'REVIEWER AGENT' in prompt.upper():
|
||||
if phase not in ['REVIEWING', 'AWAITING_IMPL_APPROVAL']:
|
||||
return False, f"""
|
||||
⛔ WORKFLOW VIOLATION: Cannot spawn Reviewer agent
|
||||
|
||||
Current Phase: {phase}
|
||||
Required Phase: REVIEWING
|
||||
|
||||
👉 REQUIRED ACTION: Complete implementation first.
|
||||
"""
|
||||
|
||||
# Check security agent
|
||||
if 'security' in agent_type or 'SECURITY AGENT' in prompt.upper():
|
||||
if phase not in ['SECURITY_REVIEW', 'REVIEWING']:
|
||||
return False, f"""
|
||||
⛔ WORKFLOW VIOLATION: Cannot spawn Security agent
|
||||
|
||||
Current Phase: {phase}
|
||||
Required Phase: SECURITY_REVIEW
|
||||
|
||||
👉 REQUIRED ACTION: Complete code review first, then security review runs.
|
||||
"""
|
||||
|
||||
return True, ""
|
||||
|
||||
|
||||
def validate_write_operation(tool_input: dict) -> tuple[bool, str]:
|
||||
"""
|
||||
Validate Write/Edit operations for workflow compliance.
|
||||
"""
|
||||
phase = get_current_phase()
|
||||
file_path = tool_input.get('file_path', tool_input.get('path', ''))
|
||||
|
||||
if not file_path:
|
||||
return True, ""
|
||||
|
||||
# Normalize path
|
||||
try:
|
||||
abs_file_path = str(Path(file_path).resolve())
|
||||
project_dir = str(Path.cwd().resolve())
|
||||
|
||||
if abs_file_path.startswith(project_dir):
|
||||
rel_path = abs_file_path[len(project_dir):].lstrip('/')
|
||||
else:
|
||||
rel_path = file_path
|
||||
except:
|
||||
rel_path = file_path
|
||||
|
||||
# Always allow these
|
||||
always_allowed = [
|
||||
'project_manifest.json',
|
||||
'.workflow/',
|
||||
'skills/',
|
||||
'.claude/',
|
||||
'CLAUDE.md',
|
||||
'package.json',
|
||||
'package-lock.json',
|
||||
'docs/', # Documentation generation (/eureka:index, /eureka:landing)
|
||||
'claudedocs/', # Claude-specific documentation
|
||||
'public/', # Public assets (landing pages, images)
|
||||
]
|
||||
|
||||
for allowed in always_allowed:
|
||||
if rel_path.startswith(allowed) or rel_path == allowed.rstrip('/'):
|
||||
return True, ""
|
||||
|
||||
# Extract feature suggestion from file path
|
||||
suggested_feature = extract_feature_from_file(rel_path)
|
||||
|
||||
# NO_WORKFLOW - Must start a workflow first
|
||||
if phase == 'NO_WORKFLOW':
|
||||
return False, f"""
|
||||
⛔ WORKFLOW REQUIRED: No active workflow
|
||||
|
||||
You are trying to modify: {rel_path}
|
||||
|
||||
This project uses guardrail workflows. You cannot directly edit files.
|
||||
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ 👉 REQUIRED ACTION: Start a workflow first! ║
|
||||
║ ║
|
||||
║ Run this command: ║
|
||||
║ /workflow:spawn {suggested_feature} ║
|
||||
║ ║
|
||||
║ This will: ║
|
||||
║ 1. Create a design for your changes ║
|
||||
║ 2. Get approval ║
|
||||
║ 3. Then allow you to implement ║
|
||||
╚══════════════════════════════════════════════════════════════════╝
|
||||
"""
|
||||
|
||||
# DESIGNING phase - can't write implementation files
|
||||
if phase == 'DESIGNING':
|
||||
return False, f"""
|
||||
⛔ WORKFLOW VIOLATION: Cannot write implementation files during DESIGNING
|
||||
|
||||
Current Phase: DESIGNING
|
||||
File: {rel_path}
|
||||
|
||||
During DESIGNING phase, only these files can be modified:
|
||||
- project_manifest.json
|
||||
- .workflow/versions/*/tasks/*.yml
|
||||
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ 👉 REQUIRED ACTION: Complete design and get approval ║
|
||||
║ ║
|
||||
║ 1. Finish adding entities to project_manifest.json ║
|
||||
║ 2. Create task files in .workflow/versions/*/tasks/ ║
|
||||
║ 3. Run: /workflow:approve ║
|
||||
╚══════════════════════════════════════════════════════════════════╝
|
||||
"""
|
||||
|
||||
# REVIEWING phase - read only
|
||||
if phase == 'REVIEWING':
|
||||
return False, f"""
|
||||
⛔ WORKFLOW VIOLATION: Cannot write files during REVIEWING
|
||||
|
||||
Current Phase: REVIEWING
|
||||
File: {rel_path}
|
||||
|
||||
During REVIEWING phase, files are READ-ONLY.
|
||||
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ 👉 REQUIRED ACTION: Complete the review ║
|
||||
║ ║
|
||||
║ If changes are needed: ║
|
||||
║ - Run: /workflow:reject "reason for changes" ║
|
||||
║ - This returns to IMPLEMENTING phase ║
|
||||
║ ║
|
||||
║ If review passes: ║
|
||||
║ - Run: /workflow:approve ║
|
||||
╚══════════════════════════════════════════════════════════════════╝
|
||||
"""
|
||||
|
||||
# SECURITY_REVIEW phase - read only
|
||||
if phase == 'SECURITY_REVIEW':
|
||||
return False, f"""
|
||||
⛔ WORKFLOW VIOLATION: Cannot write files during SECURITY_REVIEW
|
||||
|
||||
Current Phase: SECURITY_REVIEW
|
||||
File: {rel_path}
|
||||
|
||||
During SECURITY_REVIEW phase, files are READ-ONLY.
|
||||
Security scan is running to check for vulnerabilities.
|
||||
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ 👉 REQUIRED ACTION: Wait for security scan to complete ║
|
||||
║ ║
|
||||
║ If security issues found: ║
|
||||
║ - Workflow returns to IMPLEMENTING phase to fix issues ║
|
||||
║ ║
|
||||
║ If security passes: ║
|
||||
║ - Workflow proceeds to AWAITING_IMPL_APPROVAL ║
|
||||
║ ║
|
||||
║ For full audit: /workflow:security --full ║
|
||||
╚══════════════════════════════════════════════════════════════════╝
|
||||
"""
|
||||
|
||||
# AWAITING approval phases
|
||||
if phase in ['AWAITING_DESIGN_APPROVAL', 'AWAITING_IMPL_APPROVAL']:
|
||||
gate_type = "design" if "DESIGN" in phase else "implementation"
|
||||
return False, f"""
|
||||
⛔ WORKFLOW VIOLATION: Cannot write files while awaiting approval
|
||||
|
||||
Current Phase: {phase}
|
||||
File: {rel_path}
|
||||
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ 👉 REQUIRED ACTION: Get user approval ║
|
||||
║ ║
|
||||
║ Waiting for {gate_type} approval. Ask the user to run: ║
|
||||
║ - /workflow:approve (to proceed) ║
|
||||
║ - /workflow:reject (to revise) ║
|
||||
╚══════════════════════════════════════════════════════════════════╝
|
||||
"""
|
||||
|
||||
# COMPLETED - need new workflow
|
||||
if phase == 'COMPLETED':
|
||||
return False, f"""
|
||||
⛔ WORKFLOW VIOLATION: Workflow already completed
|
||||
|
||||
Current Phase: COMPLETED
|
||||
File: {rel_path}
|
||||
|
||||
This workflow version is complete.
|
||||
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ 👉 REQUIRED ACTION: Start a new workflow ║
|
||||
║ ║
|
||||
║ Run: /workflow:spawn {suggested_feature} ║
|
||||
╚══════════════════════════════════════════════════════════════════╝
|
||||
"""
|
||||
|
||||
return True, ""
|
||||
|
||||
|
||||
def validate_transition(tool_input: dict) -> tuple[bool, str]:
|
||||
"""Validate phase transitions for proper sequencing."""
|
||||
return True, ""
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Workflow enforcement hook")
|
||||
parser.add_argument('--operation', required=True,
|
||||
choices=['task', 'write', 'edit', 'transition', 'build'],
|
||||
help='Operation type being validated')
|
||||
parser.add_argument('--input', help='JSON input from tool call')
|
||||
parser.add_argument('--file', help='File path (for write/edit operations)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Parse input
|
||||
tool_input = {}
|
||||
if args.input:
|
||||
try:
|
||||
tool_input = json.loads(args.input)
|
||||
except json.JSONDecodeError:
|
||||
tool_input = {'raw': args.input}
|
||||
|
||||
if args.file:
|
||||
tool_input['file_path'] = args.file
|
||||
|
||||
# Route to appropriate validator
|
||||
allowed = True
|
||||
message = ""
|
||||
|
||||
if args.operation == 'task':
|
||||
allowed, message = validate_task_spawn(tool_input)
|
||||
|
||||
elif args.operation in ['write', 'edit']:
|
||||
allowed, message = validate_write_operation(tool_input)
|
||||
|
||||
elif args.operation == 'transition':
|
||||
allowed, message = validate_transition(tool_input)
|
||||
|
||||
elif args.operation == 'build':
|
||||
phase = get_current_phase()
|
||||
print(f"BUILD: Running in phase {phase}")
|
||||
allowed = True
|
||||
|
||||
# Output result
|
||||
if not allowed:
|
||||
print(message, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
phase = get_current_phase()
|
||||
version = get_active_version() or 'N/A'
|
||||
print(f"✓ WORKFLOW: {args.operation.upper()} allowed in {phase} (v{version})")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Pre-write validation hook for guardrail enforcement.
|
||||
|
||||
Validates that file writes are allowed based on:
|
||||
1. Current workflow phase
|
||||
2. Manifest-defined allowed paths
|
||||
3. Always-allowed system paths
|
||||
|
||||
Exit codes:
|
||||
0 = Write allowed
|
||||
1 = Write blocked (with error message)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# Always allowed paths (relative to project root)
|
||||
ALWAYS_ALLOWED_PATTERNS = [
|
||||
"project_manifest.json",
|
||||
".workflow/",
|
||||
".claude/",
|
||||
"skills/",
|
||||
"CLAUDE.md",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"tsconfig.json",
|
||||
".gitignore",
|
||||
".env.local",
|
||||
".env.example",
|
||||
"docs/", # Documentation generation (/eureka:index, /eureka:landing)
|
||||
"claudedocs/", # Claude-specific documentation
|
||||
"public/", # Public assets (landing pages, images)
|
||||
]
|
||||
|
||||
|
||||
def load_manifest(manifest_path: str) -> dict | None:
|
||||
"""Load manifest if it exists."""
|
||||
if not os.path.exists(manifest_path):
|
||||
return None
|
||||
try:
|
||||
with open(manifest_path) as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, IOError):
|
||||
return None
|
||||
|
||||
|
||||
def normalize_path(file_path: str, project_dir: str) -> str:
|
||||
"""Normalize file path to relative path from project root."""
|
||||
try:
|
||||
abs_path = Path(file_path).resolve()
|
||||
proj_path = Path(project_dir).resolve()
|
||||
|
||||
# Make relative if under project
|
||||
if str(abs_path).startswith(str(proj_path)):
|
||||
return str(abs_path.relative_to(proj_path))
|
||||
return str(abs_path)
|
||||
except (ValueError, OSError):
|
||||
return file_path
|
||||
|
||||
|
||||
def is_always_allowed(rel_path: str) -> bool:
|
||||
"""Check if path is in always-allowed list."""
|
||||
for pattern in ALWAYS_ALLOWED_PATTERNS:
|
||||
if pattern.endswith('/'):
|
||||
# Directory pattern
|
||||
if rel_path.startswith(pattern) or rel_path == pattern.rstrip('/'):
|
||||
return True
|
||||
else:
|
||||
# Exact file match
|
||||
if rel_path == pattern:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_allowed_paths_from_manifest(manifest: dict) -> set:
|
||||
"""Extract all allowed file paths from manifest entities."""
|
||||
allowed = set()
|
||||
|
||||
entities = manifest.get("entities", {})
|
||||
entity_types = ["pages", "components", "api_endpoints", "database_tables", "services", "utils", "hooks", "types"]
|
||||
|
||||
for entity_type in entity_types:
|
||||
for entity in entities.get(entity_type, []):
|
||||
status = entity.get("status", "")
|
||||
# Allow APPROVED, IMPLEMENTED, or PENDING (for design phase updates)
|
||||
if status in ["APPROVED", "IMPLEMENTED", "PENDING", "IN_PROGRESS"]:
|
||||
if "file_path" in entity:
|
||||
allowed.add(entity["file_path"])
|
||||
# Also check for multiple file paths
|
||||
if "file_paths" in entity:
|
||||
for fp in entity.get("file_paths", []):
|
||||
allowed.add(fp)
|
||||
|
||||
return allowed
|
||||
|
||||
|
||||
def get_allowed_paths_from_tasks(project_dir: str) -> set:
|
||||
"""Extract allowed file paths from task files in active workflow version."""
|
||||
allowed = set()
|
||||
|
||||
# Try to import yaml
|
||||
try:
|
||||
import yaml
|
||||
has_yaml = True
|
||||
except ImportError:
|
||||
has_yaml = False
|
||||
|
||||
# Find active version
|
||||
current_path = Path(project_dir) / ".workflow" / "current.yml"
|
||||
if not current_path.exists():
|
||||
return allowed
|
||||
|
||||
try:
|
||||
with open(current_path) as f:
|
||||
content = f.read()
|
||||
|
||||
if has_yaml:
|
||||
current = yaml.safe_load(content) or {}
|
||||
else:
|
||||
# Simple fallback parser
|
||||
current = {}
|
||||
for line in content.split('\n'):
|
||||
if ':' in line and not line.startswith(' '):
|
||||
key, _, value = line.partition(':')
|
||||
current[key.strip()] = value.strip()
|
||||
|
||||
active_version = current.get('active_version')
|
||||
if not active_version:
|
||||
return allowed
|
||||
|
||||
# Read task files
|
||||
tasks_dir = Path(project_dir) / ".workflow" / "versions" / active_version / "tasks"
|
||||
if not tasks_dir.exists():
|
||||
return allowed
|
||||
|
||||
for task_file in tasks_dir.glob("*.yml"):
|
||||
try:
|
||||
with open(task_file) as f:
|
||||
task_content = f.read()
|
||||
|
||||
if has_yaml:
|
||||
task = yaml.safe_load(task_content) or {}
|
||||
file_paths = task.get('file_paths', [])
|
||||
for fp in file_paths:
|
||||
allowed.add(fp)
|
||||
else:
|
||||
# Simple extraction for file_paths
|
||||
in_file_paths = False
|
||||
for line in task_content.split('\n'):
|
||||
if line.strip().startswith('file_paths:'):
|
||||
in_file_paths = True
|
||||
continue
|
||||
if in_file_paths:
|
||||
if line.strip().startswith('- '):
|
||||
fp = line.strip()[2:].strip()
|
||||
allowed.add(fp)
|
||||
elif not line.startswith(' '):
|
||||
in_file_paths = False
|
||||
except (IOError, Exception):
|
||||
continue
|
||||
|
||||
except (IOError, Exception):
|
||||
pass
|
||||
|
||||
return allowed
|
||||
|
||||
|
||||
def validate_write(file_path: str, manifest_path: str) -> tuple[bool, str]:
|
||||
"""
|
||||
Validate if a write operation is allowed.
|
||||
|
||||
Returns:
|
||||
(allowed: bool, message: str)
|
||||
"""
|
||||
project_dir = os.path.dirname(manifest_path) or os.getcwd()
|
||||
rel_path = normalize_path(file_path, project_dir)
|
||||
|
||||
# Check always-allowed paths first
|
||||
if is_always_allowed(rel_path):
|
||||
return True, f"✓ GUARDRAIL: Always-allowed path: {rel_path}"
|
||||
|
||||
# Load manifest
|
||||
manifest = load_manifest(manifest_path)
|
||||
|
||||
# If no manifest exists, guardrails not active
|
||||
if manifest is None:
|
||||
return True, "✓ GUARDRAIL: No manifest found, allowing write"
|
||||
|
||||
# Get current phase
|
||||
phase = manifest.get("state", {}).get("current_phase", "UNKNOWN")
|
||||
|
||||
# Collect all allowed paths
|
||||
allowed_from_manifest = get_allowed_paths_from_manifest(manifest)
|
||||
allowed_from_tasks = get_allowed_paths_from_tasks(project_dir)
|
||||
all_allowed = allowed_from_manifest | allowed_from_tasks
|
||||
|
||||
# Check if file is in allowed paths
|
||||
if rel_path in all_allowed:
|
||||
return True, f"✓ GUARDRAIL: Allowed in manifest/tasks: {rel_path}"
|
||||
|
||||
# Also check with leading ./ removed
|
||||
clean_path = rel_path.lstrip('./')
|
||||
if clean_path in all_allowed:
|
||||
return True, f"✓ GUARDRAIL: Allowed in manifest/tasks: {clean_path}"
|
||||
|
||||
# Check if any allowed path matches (handle path variations)
|
||||
for allowed in all_allowed:
|
||||
allowed_clean = allowed.lstrip('./')
|
||||
if clean_path == allowed_clean:
|
||||
return True, f"✓ GUARDRAIL: Allowed (path match): {rel_path}"
|
||||
|
||||
# Extract suggested feature from file path
|
||||
name = Path(rel_path).stem
|
||||
suggested_feature = f"update {name}"
|
||||
|
||||
# Not allowed - generate helpful error message with actionable instructions
|
||||
error_msg = f"""
|
||||
⛔ GUARDRAIL VIOLATION: Unauthorized file write
|
||||
|
||||
File: {rel_path}
|
||||
Phase: {phase}
|
||||
|
||||
This file is not in the approved manifest or task files.
|
||||
|
||||
Allowed paths from manifest: {len(allowed_from_manifest)}
|
||||
Allowed paths from tasks: {len(allowed_from_tasks)}
|
||||
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ 👉 REQUIRED ACTION: Start a workflow to modify this file ║
|
||||
║ ║
|
||||
║ Run this command: ║
|
||||
║ /workflow:spawn {suggested_feature} ║
|
||||
║ ║
|
||||
║ This will: ║
|
||||
║ 1. Design what changes are needed ║
|
||||
║ 2. Add this file to approved paths ║
|
||||
║ 3. Get approval, then implement ║
|
||||
╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
Alternative: If workflow exists, add this file to:
|
||||
- project_manifest.json (entities.*.file_path)
|
||||
- .workflow/versions/*/tasks/*.yml (file_paths list)
|
||||
"""
|
||||
|
||||
return False, error_msg
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Validate write operation against guardrails")
|
||||
parser.add_argument("--manifest", required=True, help="Path to project_manifest.json")
|
||||
parser.add_argument("--file", help="File path being written")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Get file path from argument or environment
|
||||
file_path = args.file or os.environ.get('TOOL_INPUT_FILE_PATH', '')
|
||||
|
||||
if not file_path:
|
||||
# Try reading from stdin
|
||||
if not sys.stdin.isatty():
|
||||
file_path = sys.stdin.read().strip()
|
||||
|
||||
if not file_path:
|
||||
print("✓ GUARDRAIL: No file path provided, allowing (hook misconfiguration?)")
|
||||
return 0
|
||||
|
||||
allowed, message = validate_write(file_path, args.manifest)
|
||||
|
||||
if allowed:
|
||||
print(message)
|
||||
return 0
|
||||
else:
|
||||
print(message, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Static analysis for async/await issues in TypeScript/JavaScript.
|
||||
|
||||
Catches common mistakes:
|
||||
- fetch() without await
|
||||
- .json() without await
|
||||
- Async function calls without await
|
||||
- Floating promises (promise not handled)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
# ============================================================================
|
||||
# Async Pattern Detection
|
||||
# ============================================================================
|
||||
|
||||
ASYNC_ISSUES = [
|
||||
# fetch without await - but allow .then() chains
|
||||
{
|
||||
"pattern": r"(?<!await\s)(?<!return\s)fetch\s*\([^)]*\)(?!\s*\.then)(?!\s*\))",
|
||||
"severity": "HIGH",
|
||||
"message": "fetch() without await or .then()",
|
||||
"fix": "Add 'await' before fetch() or use .then().catch()"
|
||||
},
|
||||
# .json() without await
|
||||
{
|
||||
"pattern": r"(?<!await\s)\.json\s*\(\s*\)(?!\s*\.then)",
|
||||
"severity": "HIGH",
|
||||
"message": ".json() without await",
|
||||
"fix": "Add 'await' before .json() call"
|
||||
},
|
||||
# .text() without await
|
||||
{
|
||||
"pattern": r"(?<!await\s)\.text\s*\(\s*\)(?!\s*\.then)",
|
||||
"severity": "MEDIUM",
|
||||
"message": ".text() without await",
|
||||
"fix": "Add 'await' before .text() call"
|
||||
},
|
||||
# axios/fetch response access without await
|
||||
{
|
||||
"pattern": r"(?<!await\s)(axios\.(get|post|put|delete|patch))\s*\([^)]*\)\.data",
|
||||
"severity": "HIGH",
|
||||
"message": "Accessing .data on unawaited axios call",
|
||||
"fix": "Add 'await' or use (await axios.get(...)).data"
|
||||
},
|
||||
# Promise.all without await
|
||||
{
|
||||
"pattern": r"(?<!await\s)(?<!return\s)Promise\.(all|allSettled|race|any)\s*\(",
|
||||
"severity": "HIGH",
|
||||
"message": "Promise.all/race without await",
|
||||
"fix": "Add 'await' before Promise.all()"
|
||||
},
|
||||
# Async function call patterns (common API functions)
|
||||
{
|
||||
"pattern": r"(?<!await\s)(?<!return\s)(createUser|updateUser|deleteUser|getUser|saveData|loadData|fetchData|submitForm|handleSubmit)\s*\([^)]*\)\s*;",
|
||||
"severity": "MEDIUM",
|
||||
"message": "Async function call may need await",
|
||||
"fix": "Check if this function is async and add 'await' if needed"
|
||||
},
|
||||
# setState with async value without await
|
||||
{
|
||||
"pattern": r"set\w+\s*\(\s*(?:await\s+)?fetch\s*\(",
|
||||
"severity": "HIGH",
|
||||
"message": "Setting state with fetch result - ensure await is used",
|
||||
"fix": "Use: const data = await fetch(...); setData(data)"
|
||||
},
|
||||
# useEffect with async but no await
|
||||
{
|
||||
"pattern": r"useEffect\s*\(\s*\(\s*\)\s*=>\s*\{[^}]*fetch\s*\([^}]*\}\s*,",
|
||||
"severity": "MEDIUM",
|
||||
"message": "useEffect with fetch - check async handling",
|
||||
"fix": "Create inner async function: useEffect(() => { const load = async () => {...}; load(); }, [])"
|
||||
},
|
||||
]
|
||||
|
||||
# Files/patterns to skip
|
||||
SKIP_PATTERNS = [
|
||||
r"node_modules",
|
||||
r"\.next",
|
||||
r"dist",
|
||||
r"build",
|
||||
r"\.test\.",
|
||||
r"\.spec\.",
|
||||
r"__tests__",
|
||||
r"__mocks__",
|
||||
]
|
||||
|
||||
|
||||
def should_skip_file(file_path: str) -> bool:
|
||||
"""Check if file should be skipped."""
|
||||
for pattern in SKIP_PATTERNS:
|
||||
if re.search(pattern, file_path):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def analyze_file(file_path: Path) -> List[Dict]:
|
||||
"""Analyze a single file for async issues."""
|
||||
issues = []
|
||||
|
||||
try:
|
||||
content = file_path.read_text(encoding='utf-8')
|
||||
except Exception:
|
||||
return issues
|
||||
|
||||
lines = content.split('\n')
|
||||
|
||||
for line_num, line in enumerate(lines, 1):
|
||||
# Skip comments
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('//') or stripped.startswith('*'):
|
||||
continue
|
||||
|
||||
for rule in ASYNC_ISSUES:
|
||||
if re.search(rule["pattern"], line):
|
||||
# Additional context check - skip if line has .then or .catch nearby
|
||||
context = '\n'.join(lines[max(0, line_num-2):min(len(lines), line_num+2)])
|
||||
if '.then(' in context and '.catch(' in context:
|
||||
continue
|
||||
|
||||
issues.append({
|
||||
"file": str(file_path),
|
||||
"line": line_num,
|
||||
"severity": rule["severity"],
|
||||
"message": rule["message"],
|
||||
"fix": rule["fix"],
|
||||
"code": line.strip()[:80]
|
||||
})
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def analyze_project(root_dir: str = ".") -> List[Dict]:
|
||||
"""Analyze all TypeScript/JavaScript files in project."""
|
||||
all_issues = []
|
||||
|
||||
extensions = [".ts", ".tsx", ".js", ".jsx"]
|
||||
root = Path(root_dir)
|
||||
|
||||
for ext in extensions:
|
||||
for file_path in root.rglob(f"*{ext}"):
|
||||
if should_skip_file(str(file_path)):
|
||||
continue
|
||||
issues = analyze_file(file_path)
|
||||
all_issues.extend(issues)
|
||||
|
||||
return all_issues
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Output Formatting
|
||||
# ============================================================================
|
||||
|
||||
def format_text(issues: List[Dict]) -> str:
|
||||
"""Format issues as readable text."""
|
||||
if not issues:
|
||||
return "\n✅ No async/await issues found.\n"
|
||||
|
||||
lines = []
|
||||
lines.append("")
|
||||
lines.append("╔" + "═" * 70 + "╗")
|
||||
lines.append("║" + " ASYNC/AWAIT VERIFICATION".ljust(70) + "║")
|
||||
lines.append("╠" + "═" * 70 + "╣")
|
||||
|
||||
high = [i for i in issues if i["severity"] == "HIGH"]
|
||||
medium = [i for i in issues if i["severity"] == "MEDIUM"]
|
||||
|
||||
lines.append("║" + f" 🔴 High: {len(high)} issues".ljust(70) + "║")
|
||||
lines.append("║" + f" 🟡 Medium: {len(medium)} issues".ljust(70) + "║")
|
||||
|
||||
if high:
|
||||
lines.append("╠" + "═" * 70 + "╣")
|
||||
lines.append("║" + " 🔴 HIGH SEVERITY".ljust(70) + "║")
|
||||
for issue in high:
|
||||
loc = f"{issue['file']}:{issue['line']}"
|
||||
lines.append("║" + f" {loc}".ljust(70) + "║")
|
||||
lines.append("║" + f" ❌ {issue['message']}".ljust(70) + "║")
|
||||
lines.append("║" + f" 💡 {issue['fix']}".ljust(70) + "║")
|
||||
code = issue['code'][:55]
|
||||
lines.append("║" + f" 📝 {code}".ljust(70) + "║")
|
||||
|
||||
if medium:
|
||||
lines.append("╠" + "═" * 70 + "╣")
|
||||
lines.append("║" + " 🟡 MEDIUM SEVERITY".ljust(70) + "║")
|
||||
for issue in medium[:5]: # Limit display
|
||||
loc = f"{issue['file']}:{issue['line']}"
|
||||
lines.append("║" + f" {loc}".ljust(70) + "║")
|
||||
lines.append("║" + f" ⚠️ {issue['message']}".ljust(70) + "║")
|
||||
|
||||
lines.append("╚" + "═" * 70 + "╝")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Check for async/await issues")
|
||||
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
||||
parser.add_argument("--path", default=".", help="Project path to analyze")
|
||||
parser.add_argument("--strict", action="store_true", help="Fail on any issue")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print("Scanning for async/await issues...")
|
||||
issues = analyze_project(args.path)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps({
|
||||
"issues": issues,
|
||||
"summary": {
|
||||
"high": len([i for i in issues if i["severity"] == "HIGH"]),
|
||||
"medium": len([i for i in issues if i["severity"] == "MEDIUM"]),
|
||||
"total": len(issues)
|
||||
}
|
||||
}, indent=2))
|
||||
else:
|
||||
print(format_text(issues))
|
||||
|
||||
# Exit codes
|
||||
high_count = len([i for i in issues if i["severity"] == "HIGH"])
|
||||
|
||||
if high_count > 0:
|
||||
sys.exit(1) # High severity issues found
|
||||
elif args.strict and issues:
|
||||
sys.exit(1) # Any issues in strict mode
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,376 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Verify implementation matches manifest specifications."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class VerificationResult:
|
||||
entity_id: str
|
||||
entity_type: str
|
||||
file_path: str
|
||||
exists: bool
|
||||
issues: list[str]
|
||||
warnings: list[str]
|
||||
|
||||
|
||||
def load_manifest(manifest_path: str) -> dict:
|
||||
"""Load manifest from file."""
|
||||
with open(manifest_path) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def check_file_exists(project_root: str, file_path: str) -> bool:
|
||||
"""Check if implementation file exists."""
|
||||
full_path = os.path.join(project_root, file_path)
|
||||
return os.path.exists(full_path)
|
||||
|
||||
|
||||
def read_file_content(project_root: str, file_path: str) -> Optional[str]:
|
||||
"""Read file content if it exists."""
|
||||
full_path = os.path.join(project_root, file_path)
|
||||
if not os.path.exists(full_path):
|
||||
return None
|
||||
with open(full_path, 'r') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def verify_component(project_root: str, component: dict) -> VerificationResult:
|
||||
"""Verify a component implementation matches manifest."""
|
||||
issues = []
|
||||
warnings = []
|
||||
file_path = component.get("file_path", "")
|
||||
|
||||
exists = check_file_exists(project_root, file_path)
|
||||
|
||||
if not exists:
|
||||
issues.append(f"File not found: {file_path}")
|
||||
return VerificationResult(
|
||||
entity_id=component.get("id", "unknown"),
|
||||
entity_type="component",
|
||||
file_path=file_path,
|
||||
exists=False,
|
||||
issues=issues,
|
||||
warnings=warnings
|
||||
)
|
||||
|
||||
content = read_file_content(project_root, file_path)
|
||||
if not content:
|
||||
issues.append("Could not read file content")
|
||||
return VerificationResult(
|
||||
entity_id=component.get("id", "unknown"),
|
||||
entity_type="component",
|
||||
file_path=file_path,
|
||||
exists=True,
|
||||
issues=issues,
|
||||
warnings=warnings
|
||||
)
|
||||
|
||||
# Check component name exists
|
||||
name = component.get("name", "")
|
||||
if name:
|
||||
# Check for function/const declaration or export
|
||||
patterns = [
|
||||
rf"export\s+(const|function)\s+{name}",
|
||||
rf"(const|function)\s+{name}",
|
||||
rf"export\s+\{{\s*{name}\s*\}}",
|
||||
]
|
||||
found = any(re.search(p, content) for p in patterns)
|
||||
if not found:
|
||||
issues.append(f"Component '{name}' not found in file")
|
||||
|
||||
# Check props interface
|
||||
props = component.get("props", {})
|
||||
if props:
|
||||
# Check if props interface exists
|
||||
interface_pattern = rf"interface\s+{name}Props"
|
||||
if not re.search(interface_pattern, content):
|
||||
warnings.append(f"Props interface '{name}Props' not found")
|
||||
|
||||
# Check each prop exists in the file
|
||||
for prop_name, prop_spec in props.items():
|
||||
if prop_name not in content:
|
||||
warnings.append(f"Prop '{prop_name}' may not be implemented")
|
||||
|
||||
return VerificationResult(
|
||||
entity_id=component.get("id", "unknown"),
|
||||
entity_type="component",
|
||||
file_path=file_path,
|
||||
exists=True,
|
||||
issues=issues,
|
||||
warnings=warnings
|
||||
)
|
||||
|
||||
|
||||
def verify_page(project_root: str, page: dict) -> VerificationResult:
|
||||
"""Verify a page implementation matches manifest."""
|
||||
issues = []
|
||||
warnings = []
|
||||
file_path = page.get("file_path", "")
|
||||
|
||||
exists = check_file_exists(project_root, file_path)
|
||||
|
||||
if not exists:
|
||||
issues.append(f"File not found: {file_path}")
|
||||
return VerificationResult(
|
||||
entity_id=page.get("id", "unknown"),
|
||||
entity_type="page",
|
||||
file_path=file_path,
|
||||
exists=False,
|
||||
issues=issues,
|
||||
warnings=warnings
|
||||
)
|
||||
|
||||
content = read_file_content(project_root, file_path)
|
||||
if not content:
|
||||
issues.append("Could not read file content")
|
||||
return VerificationResult(
|
||||
entity_id=page.get("id", "unknown"),
|
||||
entity_type="page",
|
||||
file_path=file_path,
|
||||
exists=True,
|
||||
issues=issues,
|
||||
warnings=warnings
|
||||
)
|
||||
|
||||
# Check for default export (Next.js page requirement)
|
||||
if "export default" not in content:
|
||||
issues.append("Missing 'export default' (required for Next.js pages)")
|
||||
|
||||
# Check component dependencies
|
||||
components = page.get("components", [])
|
||||
for comp_id in components:
|
||||
# Extract component name from ID (e.g., comp_header -> Header)
|
||||
comp_name = comp_id.replace("comp_", "").title().replace("_", "")
|
||||
if comp_name not in content:
|
||||
warnings.append(f"Component '{comp_name}' (from {comp_id}) may not be used")
|
||||
|
||||
return VerificationResult(
|
||||
entity_id=page.get("id", "unknown"),
|
||||
entity_type="page",
|
||||
file_path=file_path,
|
||||
exists=True,
|
||||
issues=issues,
|
||||
warnings=warnings
|
||||
)
|
||||
|
||||
|
||||
def verify_api_endpoint(project_root: str, endpoint: dict) -> VerificationResult:
|
||||
"""Verify an API endpoint implementation matches manifest."""
|
||||
issues = []
|
||||
warnings = []
|
||||
file_path = endpoint.get("file_path", "")
|
||||
|
||||
exists = check_file_exists(project_root, file_path)
|
||||
|
||||
if not exists:
|
||||
issues.append(f"File not found: {file_path}")
|
||||
return VerificationResult(
|
||||
entity_id=endpoint.get("id", "unknown"),
|
||||
entity_type="api_endpoint",
|
||||
file_path=file_path,
|
||||
exists=False,
|
||||
issues=issues,
|
||||
warnings=warnings
|
||||
)
|
||||
|
||||
content = read_file_content(project_root, file_path)
|
||||
if not content:
|
||||
issues.append("Could not read file content")
|
||||
return VerificationResult(
|
||||
entity_id=endpoint.get("id", "unknown"),
|
||||
entity_type="api_endpoint",
|
||||
file_path=file_path,
|
||||
exists=True,
|
||||
issues=issues,
|
||||
warnings=warnings
|
||||
)
|
||||
|
||||
# Check HTTP method handler exists
|
||||
method = endpoint.get("method", "").upper()
|
||||
method_patterns = [
|
||||
rf"export\s+async\s+function\s+{method}\s*\(",
|
||||
rf"export\s+function\s+{method}\s*\(",
|
||||
rf"export\s+const\s+{method}\s*=",
|
||||
]
|
||||
found = any(re.search(p, content) for p in method_patterns)
|
||||
if not found:
|
||||
issues.append(f"HTTP method handler '{method}' not found")
|
||||
|
||||
# Check request body params if defined
|
||||
request = endpoint.get("request", {})
|
||||
if request.get("body"):
|
||||
for param in request["body"].keys():
|
||||
if param not in content:
|
||||
warnings.append(f"Request param '{param}' may not be handled")
|
||||
|
||||
return VerificationResult(
|
||||
entity_id=endpoint.get("id", "unknown"),
|
||||
entity_type="api_endpoint",
|
||||
file_path=file_path,
|
||||
exists=True,
|
||||
issues=issues,
|
||||
warnings=warnings
|
||||
)
|
||||
|
||||
|
||||
def verify_database_table(project_root: str, table: dict) -> VerificationResult:
|
||||
"""Verify a database table implementation matches manifest."""
|
||||
issues = []
|
||||
warnings = []
|
||||
file_path = table.get("file_path", "")
|
||||
|
||||
exists = check_file_exists(project_root, file_path)
|
||||
|
||||
if not exists:
|
||||
issues.append(f"File not found: {file_path}")
|
||||
return VerificationResult(
|
||||
entity_id=table.get("id", "unknown"),
|
||||
entity_type="database_table",
|
||||
file_path=file_path,
|
||||
exists=False,
|
||||
issues=issues,
|
||||
warnings=warnings
|
||||
)
|
||||
|
||||
content = read_file_content(project_root, file_path)
|
||||
if not content:
|
||||
issues.append("Could not read file content")
|
||||
return VerificationResult(
|
||||
entity_id=table.get("id", "unknown"),
|
||||
entity_type="database_table",
|
||||
file_path=file_path,
|
||||
exists=True,
|
||||
issues=issues,
|
||||
warnings=warnings
|
||||
)
|
||||
|
||||
# Check columns/fields are defined
|
||||
columns = table.get("columns", {})
|
||||
for col_name in columns.keys():
|
||||
if col_name not in content:
|
||||
warnings.append(f"Column '{col_name}' may not be defined")
|
||||
|
||||
# Check for CRUD operations
|
||||
crud_ops = ["create", "get", "update", "delete", "find", "all"]
|
||||
found_ops = [op for op in crud_ops if op.lower() in content.lower()]
|
||||
if len(found_ops) < 2:
|
||||
warnings.append("May be missing CRUD operations")
|
||||
|
||||
return VerificationResult(
|
||||
entity_id=table.get("id", "unknown"),
|
||||
entity_type="database_table",
|
||||
file_path=file_path,
|
||||
exists=True,
|
||||
issues=issues,
|
||||
warnings=warnings
|
||||
)
|
||||
|
||||
|
||||
def print_result(result: VerificationResult, verbose: bool = False):
|
||||
"""Print verification result."""
|
||||
status = "✅" if result.exists and not result.issues else "❌"
|
||||
print(f"{status} [{result.entity_type}] {result.entity_id}")
|
||||
print(f" File: {result.file_path}")
|
||||
|
||||
if result.issues:
|
||||
for issue in result.issues:
|
||||
print(f" ❌ ERROR: {issue}")
|
||||
|
||||
if verbose and result.warnings:
|
||||
for warning in result.warnings:
|
||||
print(f" ⚠️ WARN: {warning}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Verify implementation against manifest")
|
||||
parser.add_argument("--manifest", default="project_manifest.json", help="Path to manifest")
|
||||
parser.add_argument("--project-root", default=".", help="Project root directory")
|
||||
parser.add_argument("--verbose", "-v", action="store_true", help="Show warnings")
|
||||
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
||||
args = parser.parse_args()
|
||||
|
||||
manifest_path = args.manifest
|
||||
if not os.path.isabs(manifest_path):
|
||||
manifest_path = os.path.join(args.project_root, manifest_path)
|
||||
|
||||
if not os.path.exists(manifest_path):
|
||||
print(f"Error: Manifest not found at {manifest_path}")
|
||||
return 1
|
||||
|
||||
manifest = load_manifest(manifest_path)
|
||||
entities = manifest.get("entities", {})
|
||||
|
||||
results = []
|
||||
|
||||
# Verify components
|
||||
for component in entities.get("components", []):
|
||||
result = verify_component(args.project_root, component)
|
||||
results.append(result)
|
||||
|
||||
# Verify pages
|
||||
for page in entities.get("pages", []):
|
||||
result = verify_page(args.project_root, page)
|
||||
results.append(result)
|
||||
|
||||
# Verify API endpoints
|
||||
for endpoint in entities.get("api_endpoints", []):
|
||||
result = verify_api_endpoint(args.project_root, endpoint)
|
||||
results.append(result)
|
||||
|
||||
# Verify database tables
|
||||
for table in entities.get("database_tables", []):
|
||||
result = verify_database_table(args.project_root, table)
|
||||
results.append(result)
|
||||
|
||||
# Output results
|
||||
if args.json:
|
||||
output = {
|
||||
"total": len(results),
|
||||
"passed": sum(1 for r in results if r.exists and not r.issues),
|
||||
"failed": sum(1 for r in results if not r.exists or r.issues),
|
||||
"results": [
|
||||
{
|
||||
"entity_id": r.entity_id,
|
||||
"entity_type": r.entity_type,
|
||||
"file_path": r.file_path,
|
||||
"exists": r.exists,
|
||||
"issues": r.issues,
|
||||
"warnings": r.warnings,
|
||||
}
|
||||
for r in results
|
||||
]
|
||||
}
|
||||
print(json.dumps(output, indent=2))
|
||||
else:
|
||||
print("\n" + "=" * 60)
|
||||
print("IMPLEMENTATION VERIFICATION REPORT")
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
for result in results:
|
||||
print_result(result, args.verbose)
|
||||
print()
|
||||
|
||||
# Summary
|
||||
passed = sum(1 for r in results if r.exists and not r.issues)
|
||||
failed = sum(1 for r in results if not r.exists or r.issues)
|
||||
warnings = sum(len(r.warnings) for r in results)
|
||||
|
||||
print("=" * 60)
|
||||
print(f"SUMMARY: {passed}/{len(results)} passed, {failed} failed, {warnings} warnings")
|
||||
print("=" * 60)
|
||||
|
||||
if failed > 0:
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
|
|
@ -0,0 +1,986 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Workflow versioning system with task session tracking.
|
||||
Links workflow sessions with task sessions and individual operations.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# Try to import yaml
|
||||
try:
|
||||
import yaml
|
||||
HAS_YAML = True
|
||||
except ImportError:
|
||||
HAS_YAML = False
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# YAML/JSON Helpers
|
||||
# ============================================================================
|
||||
|
||||
def load_yaml(filepath: str) -> dict:
|
||||
"""Load YAML file."""
|
||||
if not os.path.exists(filepath):
|
||||
return {}
|
||||
with open(filepath, 'r') as f:
|
||||
content = f.read()
|
||||
if not content.strip():
|
||||
return {}
|
||||
if HAS_YAML:
|
||||
return yaml.safe_load(content) or {}
|
||||
# Simple YAML fallback parser for basic key: value structures
|
||||
return parse_simple_yaml(content)
|
||||
|
||||
|
||||
def parse_simple_yaml(content: str) -> dict:
|
||||
"""Parse simple YAML without PyYAML dependency."""
|
||||
result = {}
|
||||
current_key = None
|
||||
current_list = None
|
||||
|
||||
for line in content.split('\n'):
|
||||
stripped = line.strip()
|
||||
|
||||
# Skip empty lines and comments
|
||||
if not stripped or stripped.startswith('#'):
|
||||
continue
|
||||
|
||||
# Handle list items
|
||||
if stripped.startswith('- '):
|
||||
if current_list is not None:
|
||||
value = stripped[2:].strip()
|
||||
# Handle quoted strings
|
||||
if (value.startswith('"') and value.endswith('"')) or \
|
||||
(value.startswith("'") and value.endswith("'")):
|
||||
value = value[1:-1]
|
||||
current_list.append(value)
|
||||
continue
|
||||
|
||||
# Handle key: value
|
||||
if ':' in stripped:
|
||||
key, _, value = stripped.partition(':')
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
|
||||
# Check if this is a list start
|
||||
if value == '' or value == '[]':
|
||||
current_key = key
|
||||
current_list = []
|
||||
result[key] = current_list
|
||||
elif value == '{}':
|
||||
result[key] = {}
|
||||
current_list = None
|
||||
elif value == 'null' or value == '~':
|
||||
result[key] = None
|
||||
current_list = None
|
||||
elif value == 'true':
|
||||
result[key] = True
|
||||
current_list = None
|
||||
elif value == 'false':
|
||||
result[key] = False
|
||||
current_list = None
|
||||
elif value.isdigit():
|
||||
result[key] = int(value)
|
||||
current_list = None
|
||||
else:
|
||||
# Handle quoted strings
|
||||
if (value.startswith('"') and value.endswith('"')) or \
|
||||
(value.startswith("'") and value.endswith("'")):
|
||||
value = value[1:-1]
|
||||
result[key] = value
|
||||
current_list = None
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def save_yaml(filepath: str, data: dict):
|
||||
"""Save data to YAML file."""
|
||||
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||
if HAS_YAML:
|
||||
with open(filepath, 'w') as f:
|
||||
yaml.dump(data, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
||||
else:
|
||||
with open(filepath, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
|
||||
def file_hash(filepath: str) -> str:
|
||||
"""Get SHA256 hash of file content."""
|
||||
if not os.path.exists(filepath):
|
||||
return None
|
||||
with open(filepath, 'rb') as f:
|
||||
return hashlib.sha256(f.read()).hexdigest()[:16]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Path Helpers
|
||||
# ============================================================================
|
||||
|
||||
def get_workflow_dir() -> Path:
|
||||
return Path('.workflow')
|
||||
|
||||
|
||||
def get_versions_dir() -> Path:
|
||||
return get_workflow_dir() / 'versions'
|
||||
|
||||
|
||||
def get_index_path() -> Path:
|
||||
return get_workflow_dir() / 'index.yml'
|
||||
|
||||
|
||||
def get_operations_log_path() -> Path:
|
||||
return get_workflow_dir() / 'operations.log'
|
||||
|
||||
|
||||
def get_version_dir(version: str) -> Path:
|
||||
return get_versions_dir() / version
|
||||
|
||||
|
||||
def get_current_state_path() -> Path:
|
||||
return get_workflow_dir() / 'current.yml'
|
||||
|
||||
|
||||
def get_version_tasks_dir(version: str) -> Path:
|
||||
"""Get the tasks directory for a specific version."""
|
||||
return get_version_dir(version) / 'tasks'
|
||||
|
||||
|
||||
def get_current_tasks_dir() -> Optional[Path]:
|
||||
"""Get the tasks directory for the currently active version."""
|
||||
current_path = get_current_state_path()
|
||||
if not current_path.exists():
|
||||
return None
|
||||
current = load_yaml(str(current_path))
|
||||
version = current.get('active_version')
|
||||
if not version:
|
||||
return None
|
||||
tasks_dir = get_version_tasks_dir(version)
|
||||
tasks_dir.mkdir(parents=True, exist_ok=True)
|
||||
return tasks_dir
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Version Index Management
|
||||
# ============================================================================
|
||||
|
||||
def load_index() -> dict:
|
||||
"""Load or create version index."""
|
||||
index_path = get_index_path()
|
||||
if index_path.exists():
|
||||
return load_yaml(str(index_path))
|
||||
return {
|
||||
'versions': [],
|
||||
'latest_version': None,
|
||||
'total_versions': 0
|
||||
}
|
||||
|
||||
|
||||
def save_index(index: dict):
|
||||
"""Save version index."""
|
||||
save_yaml(str(get_index_path()), index)
|
||||
|
||||
|
||||
def get_next_version() -> str:
|
||||
"""Get next version number."""
|
||||
index = load_index()
|
||||
return f"v{index['total_versions'] + 1:03d}"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Workflow Session Management
|
||||
# ============================================================================
|
||||
|
||||
def create_workflow_session(feature: str, parent_version: str = None) -> dict:
|
||||
"""Create a new workflow session with version tracking."""
|
||||
now = datetime.now()
|
||||
version = get_next_version()
|
||||
session_id = f"workflow_{now.strftime('%Y%m%d_%H%M%S')}"
|
||||
|
||||
# Create version directory and tasks subdirectory
|
||||
version_dir = get_version_dir(version)
|
||||
version_dir.mkdir(parents=True, exist_ok=True)
|
||||
(version_dir / 'tasks').mkdir(exist_ok=True)
|
||||
|
||||
# Create workflow session
|
||||
session = {
|
||||
'version': version,
|
||||
'feature': feature,
|
||||
'session_id': session_id,
|
||||
'parent_version': parent_version,
|
||||
'status': 'pending',
|
||||
'started_at': now.isoformat(),
|
||||
'completed_at': None,
|
||||
'current_phase': 'INITIALIZING',
|
||||
'approvals': {
|
||||
'design': {
|
||||
'status': 'pending',
|
||||
'approved_by': None,
|
||||
'approved_at': None,
|
||||
'rejection_reason': None
|
||||
},
|
||||
'implementation': {
|
||||
'status': 'pending',
|
||||
'approved_by': None,
|
||||
'approved_at': None,
|
||||
'rejection_reason': None
|
||||
}
|
||||
},
|
||||
'task_sessions': [],
|
||||
'summary': {
|
||||
'total_tasks': 0,
|
||||
'tasks_completed': 0,
|
||||
'entities_created': 0,
|
||||
'entities_updated': 0,
|
||||
'entities_deleted': 0,
|
||||
'files_created': 0,
|
||||
'files_updated': 0,
|
||||
'files_deleted': 0
|
||||
}
|
||||
}
|
||||
|
||||
# Save session to version directory
|
||||
save_yaml(str(version_dir / 'session.yml'), session)
|
||||
|
||||
# Update current state pointer
|
||||
get_workflow_dir().mkdir(exist_ok=True)
|
||||
save_yaml(str(get_current_state_path()), {
|
||||
'active_version': version,
|
||||
'session_id': session_id
|
||||
})
|
||||
|
||||
# Update index
|
||||
index = load_index()
|
||||
index['versions'].append({
|
||||
'version': version,
|
||||
'feature': feature,
|
||||
'status': 'pending',
|
||||
'started_at': now.isoformat(),
|
||||
'completed_at': None,
|
||||
'tasks_count': 0,
|
||||
'operations_count': 0
|
||||
})
|
||||
index['latest_version'] = version
|
||||
index['total_versions'] += 1
|
||||
save_index(index)
|
||||
|
||||
# Take snapshot of current state (manifest, tasks)
|
||||
take_snapshot(version, 'before')
|
||||
|
||||
return session
|
||||
|
||||
|
||||
def load_current_session() -> Optional[dict]:
|
||||
"""Load the current active workflow session."""
|
||||
current_path = get_current_state_path()
|
||||
if not current_path.exists():
|
||||
return None
|
||||
|
||||
current = load_yaml(str(current_path))
|
||||
version = current.get('active_version')
|
||||
if not version:
|
||||
return None
|
||||
|
||||
session_path = get_version_dir(version) / 'session.yml'
|
||||
if not session_path.exists():
|
||||
return None
|
||||
|
||||
return load_yaml(str(session_path))
|
||||
|
||||
|
||||
def save_current_session(session: dict):
|
||||
"""Save the current workflow session."""
|
||||
version = session['version']
|
||||
session['updated_at'] = datetime.now().isoformat()
|
||||
save_yaml(str(get_version_dir(version) / 'session.yml'), session)
|
||||
|
||||
# Update index
|
||||
index = load_index()
|
||||
for v in index['versions']:
|
||||
if v['version'] == version:
|
||||
v['status'] = session['status']
|
||||
v['tasks_count'] = session['summary']['total_tasks']
|
||||
break
|
||||
save_index(index)
|
||||
|
||||
|
||||
def complete_workflow_session(session: dict):
|
||||
"""Mark workflow session as completed."""
|
||||
now = datetime.now()
|
||||
session['status'] = 'completed'
|
||||
session['completed_at'] = now.isoformat()
|
||||
save_current_session(session)
|
||||
|
||||
# Take final snapshot
|
||||
take_snapshot(session['version'], 'after')
|
||||
|
||||
# Update index
|
||||
index = load_index()
|
||||
for v in index['versions']:
|
||||
if v['version'] == session['version']:
|
||||
v['status'] = 'completed'
|
||||
v['completed_at'] = now.isoformat()
|
||||
break
|
||||
save_index(index)
|
||||
|
||||
# Clear current pointer
|
||||
current_path = get_current_state_path()
|
||||
if current_path.exists():
|
||||
current_path.unlink()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Task Session Management
|
||||
# ============================================================================
|
||||
|
||||
def create_task_session(workflow_session: dict, task_id: str, task_type: str, agent: str) -> dict:
|
||||
"""Create a new task session with full directory structure."""
|
||||
now = datetime.now()
|
||||
session_id = f"tasksession_{task_id}_{now.strftime('%Y%m%d_%H%M%S')}"
|
||||
|
||||
# Create task session DIRECTORY (not file)
|
||||
version_dir = get_version_dir(workflow_session['version'])
|
||||
task_session_dir = version_dir / 'task_sessions' / task_id
|
||||
task_session_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
task_session = {
|
||||
'session_id': session_id,
|
||||
'workflow_version': workflow_session['version'],
|
||||
'task_id': task_id,
|
||||
'task_type': task_type,
|
||||
'agent': agent,
|
||||
'started_at': now.isoformat(),
|
||||
'completed_at': None,
|
||||
'duration_ms': None,
|
||||
'status': 'in_progress',
|
||||
'operations': [],
|
||||
'review_session': None,
|
||||
'errors': [],
|
||||
'attempt_number': 1,
|
||||
'previous_attempts': []
|
||||
}
|
||||
|
||||
# Save session.yml
|
||||
save_yaml(str(task_session_dir / 'session.yml'), task_session)
|
||||
|
||||
# Snapshot task definition
|
||||
snapshot_task_definition(task_id, task_session_dir)
|
||||
|
||||
# Initialize operations.log
|
||||
init_operations_log(task_session_dir, task_id, now)
|
||||
|
||||
# Link to workflow
|
||||
workflow_session['task_sessions'].append(session_id)
|
||||
workflow_session['summary']['total_tasks'] += 1
|
||||
save_current_session(workflow_session)
|
||||
|
||||
return task_session
|
||||
|
||||
|
||||
def snapshot_task_definition(task_id: str, task_session_dir: Path):
|
||||
"""Snapshot the task definition at execution time."""
|
||||
task_file = Path('tasks') / f'{task_id}.yml'
|
||||
|
||||
if task_file.exists():
|
||||
task_data = load_yaml(str(task_file))
|
||||
task_data['snapshotted_at'] = datetime.now().isoformat()
|
||||
task_data['source_path'] = str(task_file)
|
||||
task_data['status_at_snapshot'] = task_data.get('status', 'unknown')
|
||||
save_yaml(str(task_session_dir / 'task.yml'), task_data)
|
||||
|
||||
|
||||
def init_operations_log(task_session_dir: Path, task_id: str, start_time: datetime):
|
||||
"""Initialize the operations log file."""
|
||||
log_path = task_session_dir / 'operations.log'
|
||||
header = f"# Operations Log for {task_id}\n"
|
||||
header += f"# Started: {start_time.isoformat()}\n"
|
||||
header += "# Format: [timestamp] OPERATION target_type: target_id (path)\n"
|
||||
header += "=" * 70 + "\n\n"
|
||||
with open(log_path, 'w') as f:
|
||||
f.write(header)
|
||||
|
||||
|
||||
def log_to_task_operations_log(task_session: dict, operation: dict):
|
||||
"""Append operation to task-specific operations log."""
|
||||
version = task_session['workflow_version']
|
||||
task_id = task_session['task_id']
|
||||
log_path = get_version_dir(version) / 'task_sessions' / task_id / 'operations.log'
|
||||
|
||||
if not log_path.exists():
|
||||
return
|
||||
|
||||
entry = (
|
||||
f"[{operation['performed_at']}] "
|
||||
f"{operation['type']} {operation['target_type']}: {operation['target_id']}"
|
||||
)
|
||||
if operation.get('target_path'):
|
||||
entry += f" ({operation['target_path']})"
|
||||
entry += f"\n Summary: {operation['changes']['diff_summary']}\n"
|
||||
|
||||
with open(log_path, 'a') as f:
|
||||
f.write(entry + "\n")
|
||||
|
||||
|
||||
def load_task_session(version: str, task_id: str) -> Optional[dict]:
|
||||
"""Load a task session from directory or flat file (backwards compatible)."""
|
||||
# Try new directory structure first
|
||||
session_dir = get_version_dir(version) / 'task_sessions' / task_id
|
||||
session_path = session_dir / 'session.yml'
|
||||
|
||||
if session_path.exists():
|
||||
return load_yaml(str(session_path))
|
||||
|
||||
# Fallback to old flat file structure
|
||||
old_path = get_version_dir(version) / 'task_sessions' / f'{task_id}.yml'
|
||||
if old_path.exists():
|
||||
return load_yaml(str(old_path))
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def save_task_session(task_session: dict):
|
||||
"""Save a task session to directory structure."""
|
||||
version = task_session['workflow_version']
|
||||
task_id = task_session['task_id']
|
||||
session_dir = get_version_dir(version) / 'task_sessions' / task_id
|
||||
session_dir.mkdir(parents=True, exist_ok=True)
|
||||
save_yaml(str(session_dir / 'session.yml'), task_session)
|
||||
|
||||
|
||||
def complete_task_session(task_session: dict, status: str = 'completed'):
|
||||
"""Mark task session as completed."""
|
||||
now = datetime.now()
|
||||
started = datetime.fromisoformat(task_session['started_at'])
|
||||
task_session['completed_at'] = now.isoformat()
|
||||
task_session['duration_ms'] = int((now - started).total_seconds() * 1000)
|
||||
task_session['status'] = status
|
||||
save_task_session(task_session)
|
||||
|
||||
# Update workflow summary
|
||||
session = load_current_session()
|
||||
if session and status == 'completed':
|
||||
session['summary']['tasks_completed'] += 1
|
||||
save_current_session(session)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Operation Logging
|
||||
# ============================================================================
|
||||
|
||||
def log_operation(
|
||||
task_session: dict,
|
||||
op_type: str, # CREATE, UPDATE, DELETE, RENAME, MOVE
|
||||
target_type: str, # file, entity, task, manifest
|
||||
target_id: str,
|
||||
target_path: str = None,
|
||||
before_state: str = None,
|
||||
after_state: str = None,
|
||||
diff_summary: str = None,
|
||||
rollback_data: dict = None
|
||||
) -> dict:
|
||||
"""Log an operation within a task session."""
|
||||
now = datetime.now()
|
||||
seq = len(task_session['operations']) + 1
|
||||
op_id = f"op_{now.strftime('%Y%m%d_%H%M%S')}_{seq:03d}"
|
||||
|
||||
operation = {
|
||||
'id': op_id,
|
||||
'type': op_type,
|
||||
'target_type': target_type,
|
||||
'target_id': target_id,
|
||||
'target_path': target_path,
|
||||
'changes': {
|
||||
'before': before_state,
|
||||
'after': after_state,
|
||||
'diff_summary': diff_summary or f"{op_type} {target_type}: {target_id}"
|
||||
},
|
||||
'performed_at': now.isoformat(),
|
||||
'reversible': rollback_data is not None,
|
||||
'rollback_data': rollback_data
|
||||
}
|
||||
|
||||
task_session['operations'].append(operation)
|
||||
save_task_session(task_session)
|
||||
|
||||
# Update workflow summary
|
||||
session = load_current_session()
|
||||
if session:
|
||||
if op_type == 'CREATE':
|
||||
if target_type == 'file':
|
||||
session['summary']['files_created'] += 1
|
||||
elif target_type == 'entity':
|
||||
session['summary']['entities_created'] += 1
|
||||
elif op_type == 'UPDATE':
|
||||
if target_type == 'file':
|
||||
session['summary']['files_updated'] += 1
|
||||
elif target_type == 'entity':
|
||||
session['summary']['entities_updated'] += 1
|
||||
elif op_type == 'DELETE':
|
||||
if target_type == 'file':
|
||||
session['summary']['files_deleted'] += 1
|
||||
elif target_type == 'entity':
|
||||
session['summary']['entities_deleted'] += 1
|
||||
save_current_session(session)
|
||||
|
||||
# Also log to operations log
|
||||
log_to_file(operation, task_session)
|
||||
|
||||
# Also log to task-specific operations log
|
||||
log_to_task_operations_log(task_session, operation)
|
||||
|
||||
# Update index operations count
|
||||
index = load_index()
|
||||
for v in index['versions']:
|
||||
if v['version'] == task_session['workflow_version']:
|
||||
v['operations_count'] = v.get('operations_count', 0) + 1
|
||||
break
|
||||
save_index(index)
|
||||
|
||||
return operation
|
||||
|
||||
|
||||
def log_to_file(operation: dict, task_session: dict):
|
||||
"""Append operation to global operations log."""
|
||||
log_path = get_operations_log_path()
|
||||
log_entry = (
|
||||
f"[{operation['performed_at']}] "
|
||||
f"v{task_session['workflow_version']} | "
|
||||
f"{task_session['task_id']} | "
|
||||
f"{operation['type']} {operation['target_type']}: {operation['target_id']}"
|
||||
)
|
||||
if operation['target_path']:
|
||||
log_entry += f" ({operation['target_path']})"
|
||||
log_entry += "\n"
|
||||
|
||||
with open(log_path, 'a') as f:
|
||||
f.write(log_entry)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Review Session Management
|
||||
# ============================================================================
|
||||
|
||||
def create_review_session(task_session: dict, reviewer: str = 'reviewer') -> dict:
|
||||
"""Create a review session for a task."""
|
||||
now = datetime.now()
|
||||
session_id = f"review_{task_session['task_id']}_{now.strftime('%Y%m%d_%H%M%S')}"
|
||||
|
||||
review = {
|
||||
'session_id': session_id,
|
||||
'task_session_id': task_session['session_id'],
|
||||
'workflow_version': task_session['workflow_version'],
|
||||
'reviewer': reviewer,
|
||||
'started_at': now.isoformat(),
|
||||
'completed_at': None,
|
||||
'decision': None,
|
||||
'checks': {
|
||||
'file_exists': None,
|
||||
'manifest_compliance': None,
|
||||
'code_quality': None,
|
||||
'lint': None,
|
||||
'build': None,
|
||||
'tests': None
|
||||
},
|
||||
'notes': '',
|
||||
'issues_found': [],
|
||||
'suggestions': []
|
||||
}
|
||||
|
||||
task_session['review_session'] = review
|
||||
save_task_session(task_session)
|
||||
|
||||
return review
|
||||
|
||||
|
||||
def complete_review_session(
|
||||
task_session: dict,
|
||||
decision: str,
|
||||
checks: dict,
|
||||
notes: str = '',
|
||||
issues: list = None,
|
||||
suggestions: list = None
|
||||
):
|
||||
"""Complete a review session."""
|
||||
now = datetime.now()
|
||||
review = task_session['review_session']
|
||||
review['completed_at'] = now.isoformat()
|
||||
review['decision'] = decision
|
||||
review['checks'].update(checks)
|
||||
review['notes'] = notes
|
||||
review['issues_found'] = issues or []
|
||||
review['suggestions'] = suggestions or []
|
||||
|
||||
save_task_session(task_session)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Snapshots
|
||||
# ============================================================================
|
||||
|
||||
def take_snapshot(version: str, snapshot_type: str):
|
||||
"""Take a snapshot of current state (before/after)."""
|
||||
snapshot_dir = get_version_dir(version) / f'snapshot_{snapshot_type}'
|
||||
snapshot_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Snapshot manifest
|
||||
if os.path.exists('project_manifest.json'):
|
||||
shutil.copy('project_manifest.json', snapshot_dir / 'manifest.json')
|
||||
|
||||
# Snapshot tasks directory
|
||||
if os.path.exists('tasks'):
|
||||
tasks_snapshot = snapshot_dir / 'tasks'
|
||||
if tasks_snapshot.exists():
|
||||
shutil.rmtree(tasks_snapshot)
|
||||
shutil.copytree('tasks', tasks_snapshot)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# History & Diff
|
||||
# ============================================================================
|
||||
|
||||
def list_versions() -> list:
|
||||
"""List all workflow versions."""
|
||||
index = load_index()
|
||||
return index['versions']
|
||||
|
||||
|
||||
def get_version_details(version: str) -> Optional[dict]:
|
||||
"""Get detailed info about a version."""
|
||||
session_path = get_version_dir(version) / 'session.yml'
|
||||
if not session_path.exists():
|
||||
return None
|
||||
return load_yaml(str(session_path))
|
||||
|
||||
|
||||
def get_changelog(version: str) -> dict:
|
||||
"""Generate changelog for a version."""
|
||||
session = get_version_details(version)
|
||||
if not session:
|
||||
return None
|
||||
|
||||
changelog = {
|
||||
'version': version,
|
||||
'feature': session['feature'],
|
||||
'status': session['status'],
|
||||
'started_at': session['started_at'],
|
||||
'completed_at': session['completed_at'],
|
||||
'operations': {
|
||||
'created': [],
|
||||
'updated': [],
|
||||
'deleted': []
|
||||
},
|
||||
'summary': session['summary']
|
||||
}
|
||||
|
||||
# Collect operations from all task sessions
|
||||
tasks_dir = get_version_dir(version) / 'task_sessions'
|
||||
if tasks_dir.exists():
|
||||
for task_file in tasks_dir.glob('*.yml'):
|
||||
task = load_yaml(str(task_file))
|
||||
for op in task.get('operations', []):
|
||||
entry = {
|
||||
'type': op['target_type'],
|
||||
'id': op['target_id'],
|
||||
'path': op['target_path'],
|
||||
'task': task['task_id'],
|
||||
'agent': task['agent']
|
||||
}
|
||||
if op['type'] == 'CREATE':
|
||||
changelog['operations']['created'].append(entry)
|
||||
elif op['type'] == 'UPDATE':
|
||||
changelog['operations']['updated'].append(entry)
|
||||
elif op['type'] == 'DELETE':
|
||||
changelog['operations']['deleted'].append(entry)
|
||||
|
||||
return changelog
|
||||
|
||||
|
||||
def diff_versions(version1: str, version2: str) -> dict:
|
||||
"""Compare two versions."""
|
||||
v1 = get_version_details(version1)
|
||||
v2 = get_version_details(version2)
|
||||
|
||||
if not v1 or not v2:
|
||||
return None
|
||||
|
||||
return {
|
||||
'from_version': version1,
|
||||
'to_version': version2,
|
||||
'from_feature': v1['feature'],
|
||||
'to_feature': v2['feature'],
|
||||
'summary_diff': {
|
||||
'entities_created': v2['summary']['entities_created'] - v1['summary']['entities_created'],
|
||||
'entities_updated': v2['summary']['entities_updated'] - v1['summary']['entities_updated'],
|
||||
'files_created': v2['summary']['files_created'] - v1['summary']['files_created'],
|
||||
'files_updated': v2['summary']['files_updated'] - v1['summary']['files_updated']
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Display Functions
|
||||
# ============================================================================
|
||||
|
||||
def show_history():
|
||||
"""Display version history."""
|
||||
versions = list_versions()
|
||||
|
||||
print()
|
||||
print("╔" + "═" * 70 + "╗")
|
||||
print("║" + "WORKFLOW VERSION HISTORY".center(70) + "║")
|
||||
print("╠" + "═" * 70 + "╣")
|
||||
|
||||
if not versions:
|
||||
print("║" + " No workflow versions found.".ljust(70) + "║")
|
||||
else:
|
||||
for v in versions:
|
||||
status_icon = "✅" if v['status'] == 'completed' else "🔄" if v['status'] == 'in_progress' else "⏳"
|
||||
line1 = f" {status_icon} {v['version']}: {v['feature'][:45]}"
|
||||
print("║" + line1.ljust(70) + "║")
|
||||
line2 = f" Started: {v['started_at'][:19]} | Tasks: {v['tasks_count']} | Ops: {v.get('operations_count', 0)}"
|
||||
print("║" + line2.ljust(70) + "║")
|
||||
print("║" + "─" * 70 + "║")
|
||||
|
||||
print("╚" + "═" * 70 + "╝")
|
||||
|
||||
|
||||
def show_changelog(version: str):
|
||||
"""Display changelog for a version."""
|
||||
changelog = get_changelog(version)
|
||||
if not changelog:
|
||||
print(f"Version {version} not found.")
|
||||
return
|
||||
|
||||
print()
|
||||
print("╔" + "═" * 70 + "╗")
|
||||
print("║" + f"CHANGELOG: {version}".center(70) + "║")
|
||||
print("╠" + "═" * 70 + "╣")
|
||||
print("║" + f" Feature: {changelog['feature'][:55]}".ljust(70) + "║")
|
||||
print("║" + f" Status: {changelog['status']}".ljust(70) + "║")
|
||||
print("╠" + "═" * 70 + "╣")
|
||||
|
||||
ops = changelog['operations']
|
||||
print("║" + " CREATED".ljust(70) + "║")
|
||||
for item in ops['created']:
|
||||
print("║" + f" + [{item['type']}] {item['id']}".ljust(70) + "║")
|
||||
if item['path']:
|
||||
print("║" + f" {item['path']}".ljust(70) + "║")
|
||||
|
||||
print("║" + " UPDATED".ljust(70) + "║")
|
||||
for item in ops['updated']:
|
||||
print("║" + f" ~ [{item['type']}] {item['id']}".ljust(70) + "║")
|
||||
|
||||
print("║" + " DELETED".ljust(70) + "║")
|
||||
for item in ops['deleted']:
|
||||
print("║" + f" - [{item['type']}] {item['id']}".ljust(70) + "║")
|
||||
|
||||
print("╠" + "═" * 70 + "╣")
|
||||
s = changelog['summary']
|
||||
print("║" + " SUMMARY".ljust(70) + "║")
|
||||
print("║" + f" Entities: +{s['entities_created']} ~{s['entities_updated']} -{s['entities_deleted']}".ljust(70) + "║")
|
||||
print("║" + f" Files: +{s['files_created']} ~{s['files_updated']} -{s['files_deleted']}".ljust(70) + "║")
|
||||
print("╚" + "═" * 70 + "╝")
|
||||
|
||||
|
||||
def show_current():
|
||||
"""Show current active workflow."""
|
||||
session = load_current_session()
|
||||
if not session:
|
||||
print("No active workflow.")
|
||||
print("Start one with: /workflow:spawn 'feature name'")
|
||||
return
|
||||
|
||||
print()
|
||||
print("╔" + "═" * 70 + "╗")
|
||||
print("║" + "CURRENT WORKFLOW SESSION".center(70) + "║")
|
||||
print("╠" + "═" * 70 + "╣")
|
||||
print("║" + f" Version: {session['version']}".ljust(70) + "║")
|
||||
print("║" + f" Feature: {session['feature'][:55]}".ljust(70) + "║")
|
||||
print("║" + f" Phase: {session['current_phase']}".ljust(70) + "║")
|
||||
print("║" + f" Status: {session['status']}".ljust(70) + "║")
|
||||
print("╠" + "═" * 70 + "╣")
|
||||
print("║" + " APPROVALS".ljust(70) + "║")
|
||||
d = session['approvals']['design']
|
||||
i = session['approvals']['implementation']
|
||||
d_icon = "✅" if d['status'] == 'approved' else "❌" if d['status'] == 'rejected' else "⏳"
|
||||
i_icon = "✅" if i['status'] == 'approved' else "❌" if i['status'] == 'rejected' else "⏳"
|
||||
print("║" + f" {d_icon} Design: {d['status']}".ljust(70) + "║")
|
||||
print("║" + f" {i_icon} Implementation: {i['status']}".ljust(70) + "║")
|
||||
print("╠" + "═" * 70 + "╣")
|
||||
s = session['summary']
|
||||
print("║" + " PROGRESS".ljust(70) + "║")
|
||||
print("║" + f" Tasks: {s['tasks_completed']}/{s['total_tasks']} completed".ljust(70) + "║")
|
||||
print("║" + f" Entities: +{s['entities_created']} ~{s['entities_updated']} -{s['entities_deleted']}".ljust(70) + "║")
|
||||
print("║" + f" Files: +{s['files_created']} ~{s['files_updated']} -{s['files_deleted']}".ljust(70) + "║")
|
||||
print("╚" + "═" * 70 + "╝")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CLI Interface
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Workflow versioning system")
|
||||
subparsers = parser.add_subparsers(dest='command', help='Commands')
|
||||
|
||||
# create command
|
||||
create_parser = subparsers.add_parser('create', help='Create new workflow version')
|
||||
create_parser.add_argument('feature', help='Feature description')
|
||||
create_parser.add_argument('--parent', help='Parent version (for fixes)')
|
||||
|
||||
# current command
|
||||
subparsers.add_parser('current', help='Show current workflow')
|
||||
|
||||
# history command
|
||||
subparsers.add_parser('history', help='Show version history')
|
||||
|
||||
# changelog command
|
||||
changelog_parser = subparsers.add_parser('changelog', help='Show version changelog')
|
||||
changelog_parser.add_argument('version', help='Version to show')
|
||||
|
||||
# diff command
|
||||
diff_parser = subparsers.add_parser('diff', help='Compare two versions')
|
||||
diff_parser.add_argument('version1', help='First version')
|
||||
diff_parser.add_argument('version2', help='Second version')
|
||||
|
||||
# task-start command
|
||||
task_start = subparsers.add_parser('task-start', help='Start a task session')
|
||||
task_start.add_argument('task_id', help='Task ID')
|
||||
task_start.add_argument('--type', default='create', help='Task type')
|
||||
task_start.add_argument('--agent', required=True, help='Agent performing task')
|
||||
|
||||
# task-complete command
|
||||
task_complete = subparsers.add_parser('task-complete', help='Complete a task session')
|
||||
task_complete.add_argument('task_id', help='Task ID')
|
||||
task_complete.add_argument('--status', default='completed', help='Final status')
|
||||
|
||||
# log-op command
|
||||
log_op = subparsers.add_parser('log-op', help='Log an operation')
|
||||
log_op.add_argument('task_id', help='Task ID')
|
||||
log_op.add_argument('op_type', choices=['CREATE', 'UPDATE', 'DELETE'])
|
||||
log_op.add_argument('target_type', choices=['file', 'entity', 'task', 'manifest'])
|
||||
log_op.add_argument('target_id', help='Target ID')
|
||||
log_op.add_argument('--path', help='File path if applicable')
|
||||
log_op.add_argument('--summary', help='Change summary')
|
||||
|
||||
# complete command
|
||||
subparsers.add_parser('complete', help='Complete current workflow')
|
||||
|
||||
# update-phase command
|
||||
phase_parser = subparsers.add_parser('update-phase', help='Update workflow phase')
|
||||
phase_parser.add_argument('phase', help='New phase')
|
||||
|
||||
# tasks-dir command
|
||||
tasks_dir_parser = subparsers.add_parser('tasks-dir', help='Get tasks directory for current or specific version')
|
||||
tasks_dir_parser.add_argument('--version', help='Specific version (defaults to current)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == 'create':
|
||||
session = create_workflow_session(args.feature, args.parent)
|
||||
print(f"Created workflow version: {session['version']}")
|
||||
print(f"Feature: {args.feature}")
|
||||
print(f"Session ID: {session['session_id']}")
|
||||
|
||||
elif args.command == 'current':
|
||||
show_current()
|
||||
|
||||
elif args.command == 'history':
|
||||
show_history()
|
||||
|
||||
elif args.command == 'changelog':
|
||||
show_changelog(args.version)
|
||||
|
||||
elif args.command == 'diff':
|
||||
result = diff_versions(args.version1, args.version2)
|
||||
if result:
|
||||
print(json.dumps(result, indent=2))
|
||||
else:
|
||||
print("Could not compare versions")
|
||||
|
||||
elif args.command == 'task-start':
|
||||
session = load_current_session()
|
||||
if not session:
|
||||
print("Error: No active workflow")
|
||||
sys.exit(1)
|
||||
task = create_task_session(session, args.task_id, args.type, args.agent)
|
||||
print(f"Started task session: {task['session_id']}")
|
||||
|
||||
elif args.command == 'task-complete':
|
||||
session = load_current_session()
|
||||
if not session:
|
||||
print("Error: No active workflow")
|
||||
sys.exit(1)
|
||||
task = load_task_session(session['version'], args.task_id)
|
||||
if task:
|
||||
complete_task_session(task, args.status)
|
||||
print(f"Completed task: {args.task_id}")
|
||||
else:
|
||||
print(f"Task session not found: {args.task_id}")
|
||||
|
||||
elif args.command == 'log-op':
|
||||
session = load_current_session()
|
||||
if not session:
|
||||
print("Error: No active workflow")
|
||||
sys.exit(1)
|
||||
task = load_task_session(session['version'], args.task_id)
|
||||
if task:
|
||||
op = log_operation(
|
||||
task,
|
||||
args.op_type,
|
||||
args.target_type,
|
||||
args.target_id,
|
||||
target_path=args.path,
|
||||
diff_summary=args.summary
|
||||
)
|
||||
print(f"Logged operation: {op['id']}")
|
||||
else:
|
||||
print(f"Task session not found: {args.task_id}")
|
||||
|
||||
elif args.command == 'complete':
|
||||
session = load_current_session()
|
||||
if not session:
|
||||
print("Error: No active workflow")
|
||||
sys.exit(1)
|
||||
complete_workflow_session(session)
|
||||
print(f"Completed workflow: {session['version']}")
|
||||
|
||||
elif args.command == 'update-phase':
|
||||
session = load_current_session()
|
||||
if not session:
|
||||
print("Error: No active workflow")
|
||||
sys.exit(1)
|
||||
session['current_phase'] = args.phase
|
||||
save_current_session(session)
|
||||
print(f"Updated phase to: {args.phase}")
|
||||
|
||||
elif args.command == 'tasks-dir':
|
||||
if args.version:
|
||||
# Specific version requested
|
||||
tasks_dir = get_version_tasks_dir(args.version)
|
||||
tasks_dir.mkdir(parents=True, exist_ok=True)
|
||||
print(str(tasks_dir))
|
||||
else:
|
||||
# Use current version
|
||||
tasks_dir = get_current_tasks_dir()
|
||||
if tasks_dir:
|
||||
print(str(tasks_dir))
|
||||
else:
|
||||
print("Error: No active workflow")
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,343 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Design visualization for guardrail workflow.
|
||||
|
||||
Generates ASCII art visualization of pages, components, and API endpoints
|
||||
from the project manifest.
|
||||
|
||||
Usage:
|
||||
python3 visualize_design.py --manifest project_manifest.json
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_manifest(manifest_path: str) -> dict | None:
|
||||
"""Load manifest if it exists."""
|
||||
if not os.path.exists(manifest_path):
|
||||
return None
|
||||
try:
|
||||
with open(manifest_path) as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, IOError):
|
||||
return None
|
||||
|
||||
|
||||
def get_status_icon(status: str) -> str:
|
||||
"""Get icon for entity status."""
|
||||
icons = {
|
||||
'PENDING': '⏳',
|
||||
'APPROVED': '✅',
|
||||
'IMPLEMENTED': '🟢',
|
||||
'IN_PROGRESS': '🔄',
|
||||
'REJECTED': '❌',
|
||||
}
|
||||
return icons.get(status, '○')
|
||||
|
||||
|
||||
def visualize_page(page: dict, components: list, indent: str = "") -> list:
|
||||
"""Generate ASCII visualization for a page."""
|
||||
lines = []
|
||||
name = page.get('name', 'Unknown')
|
||||
status = page.get('status', 'PENDING')
|
||||
file_path = page.get('file_path', '')
|
||||
description = page.get('description', '')
|
||||
|
||||
icon = get_status_icon(status)
|
||||
|
||||
# Page header
|
||||
lines.append(f"{indent}┌{'─' * 60}┐")
|
||||
lines.append(f"{indent}│ {icon} PAGE: {name:<50} │")
|
||||
lines.append(f"{indent}│ {' ' * 3}Path: {file_path:<48} │")
|
||||
if description:
|
||||
desc_short = description[:45] + '...' if len(description) > 45 else description
|
||||
lines.append(f"{indent}│ {' ' * 3}Desc: {desc_short:<48} │")
|
||||
lines.append(f"{indent}├{'─' * 60}┤")
|
||||
|
||||
# Find components used by this page
|
||||
page_components = []
|
||||
page_id = page.get('id', '')
|
||||
for comp in components:
|
||||
deps = comp.get('dependencies', [])
|
||||
used_by = comp.get('used_by', [])
|
||||
if page_id in deps or page_id in used_by or page.get('name', '').lower() in str(comp).lower():
|
||||
page_components.append(comp)
|
||||
|
||||
if page_components:
|
||||
lines.append(f"{indent}│ COMPONENTS: │")
|
||||
for comp in page_components:
|
||||
comp_name = comp.get('name', 'Unknown')
|
||||
comp_status = comp.get('status', 'PENDING')
|
||||
comp_icon = get_status_icon(comp_status)
|
||||
lines.append(f"{indent}│ {comp_icon} {comp_name:<53} │")
|
||||
else:
|
||||
lines.append(f"{indent}│ (No components defined yet) │")
|
||||
|
||||
lines.append(f"{indent}└{'─' * 60}┘")
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def visualize_component_tree(components: list) -> list:
|
||||
"""Generate ASCII tree of components."""
|
||||
lines = []
|
||||
|
||||
if not components:
|
||||
return [" (No components defined)"]
|
||||
|
||||
lines.append("┌─────────────────────────────────────────────────────────────┐")
|
||||
lines.append("│ 🧩 COMPONENTS │")
|
||||
lines.append("├─────────────────────────────────────────────────────────────┤")
|
||||
|
||||
for i, comp in enumerate(components):
|
||||
name = comp.get('name', 'Unknown')
|
||||
status = comp.get('status', 'PENDING')
|
||||
file_path = comp.get('file_path', '')
|
||||
icon = get_status_icon(status)
|
||||
|
||||
is_last = i == len(components) - 1
|
||||
prefix = "└──" if is_last else "├──"
|
||||
|
||||
lines.append(f"│ {prefix} {icon} {name:<50} │")
|
||||
lines.append(f"│ {' ' if is_last else '│ '} {file_path:<50} │")
|
||||
|
||||
lines.append("└─────────────────────────────────────────────────────────────┘")
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def visualize_api_endpoints(endpoints: list) -> list:
|
||||
"""Generate ASCII visualization of API endpoints."""
|
||||
lines = []
|
||||
|
||||
if not endpoints:
|
||||
return []
|
||||
|
||||
lines.append("┌─────────────────────────────────────────────────────────────┐")
|
||||
lines.append("│ 🔌 API ENDPOINTS │")
|
||||
lines.append("├─────────────────────────────────────────────────────────────┤")
|
||||
|
||||
for endpoint in endpoints:
|
||||
name = endpoint.get('name', 'Unknown')
|
||||
method = endpoint.get('method', 'GET')
|
||||
path = endpoint.get('path', endpoint.get('file_path', ''))
|
||||
status = endpoint.get('status', 'PENDING')
|
||||
icon = get_status_icon(status)
|
||||
|
||||
method_colors = {
|
||||
'GET': '🟢',
|
||||
'POST': '🟡',
|
||||
'PUT': '🟠',
|
||||
'PATCH': '🟠',
|
||||
'DELETE': '🔴',
|
||||
}
|
||||
method_icon = method_colors.get(method.upper(), '⚪')
|
||||
|
||||
lines.append(f"│ {icon} {method_icon} {method.upper():<6} {name:<45} │")
|
||||
lines.append(f"│ Path: {path:<47} │")
|
||||
|
||||
lines.append("└─────────────────────────────────────────────────────────────┘")
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def visualize_page_flow(pages: list) -> list:
|
||||
"""Generate ASCII flow diagram of pages."""
|
||||
lines = []
|
||||
|
||||
if not pages:
|
||||
return []
|
||||
|
||||
lines.append("")
|
||||
lines.append("📱 PAGE FLOW DIAGRAM")
|
||||
lines.append("═" * 65)
|
||||
lines.append("")
|
||||
|
||||
# Simple flow visualization
|
||||
for i, page in enumerate(pages):
|
||||
name = page.get('name', 'Unknown')
|
||||
status = page.get('status', 'PENDING')
|
||||
icon = get_status_icon(status)
|
||||
|
||||
# Page box
|
||||
box_width = max(len(name) + 4, 20)
|
||||
lines.append(f" ┌{'─' * box_width}┐")
|
||||
lines.append(f" │ {icon} {name.center(box_width - 4)} │")
|
||||
lines.append(f" └{'─' * box_width}┘")
|
||||
|
||||
# Arrow to next page (if not last)
|
||||
if i < len(pages) - 1:
|
||||
lines.append(f" {'│'.center(box_width + 4)}")
|
||||
lines.append(f" {'▼'.center(box_width + 4)}")
|
||||
lines.append("")
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def visualize_data_flow(manifest: dict) -> list:
|
||||
"""Generate data flow visualization."""
|
||||
lines = []
|
||||
|
||||
pages = manifest.get('entities', {}).get('pages', [])
|
||||
components = manifest.get('entities', {}).get('components', [])
|
||||
endpoints = manifest.get('entities', {}).get('api_endpoints', [])
|
||||
|
||||
if not any([pages, components, endpoints]):
|
||||
return []
|
||||
|
||||
lines.append("")
|
||||
lines.append("🔄 DATA FLOW ARCHITECTURE")
|
||||
lines.append("═" * 65)
|
||||
lines.append("")
|
||||
lines.append(" ┌─────────────────────────────────────────────────────────┐")
|
||||
lines.append(" │ FRONTEND │")
|
||||
lines.append(" │ ┌─────────┐ ┌─────────────┐ ┌───────────────┐ │")
|
||||
lines.append(" │ │ Pages │───▶│ Components │───▶│ Hooks │ │")
|
||||
|
||||
page_count = len(pages)
|
||||
comp_count = len(components)
|
||||
lines.append(f" │ │ ({page_count:^3}) │ │ ({comp_count:^3}) │ │ (state) │ │")
|
||||
lines.append(" │ └─────────┘ └─────────────┘ └───────┬───────┘ │")
|
||||
lines.append(" │ │ │")
|
||||
lines.append(" └────────────────────────────────────────────┼───────────┘")
|
||||
lines.append(" │")
|
||||
lines.append(" ▼")
|
||||
lines.append(" ┌─────────────────────────────────────────────────────────┐")
|
||||
lines.append(" │ BACKEND │")
|
||||
lines.append(" │ ┌─────────────────────────────────────────────────┐ │")
|
||||
lines.append(" │ │ API Endpoints │ │")
|
||||
|
||||
api_count = len(endpoints)
|
||||
lines.append(f" │ │ ({api_count:^3}) │ │")
|
||||
lines.append(" │ └──────────────────────────┬──────────────────────┘ │")
|
||||
lines.append(" │ │ │")
|
||||
lines.append(" │ ▼ │")
|
||||
lines.append(" │ ┌─────────────────────────────────────────────────┐ │")
|
||||
lines.append(" │ │ Database │ │")
|
||||
lines.append(" │ └─────────────────────────────────────────────────┘ │")
|
||||
lines.append(" └─────────────────────────────────────────────────────────┘")
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def generate_full_visualization(manifest: dict) -> str:
|
||||
"""Generate complete design visualization."""
|
||||
lines = []
|
||||
|
||||
project_name = manifest.get('project', {}).get('name', 'Unknown Project')
|
||||
entities = manifest.get('entities', {})
|
||||
|
||||
pages = entities.get('pages', [])
|
||||
components = entities.get('components', [])
|
||||
api_endpoints = entities.get('api_endpoints', [])
|
||||
|
||||
# Header
|
||||
lines.append("")
|
||||
lines.append("╔═══════════════════════════════════════════════════════════════╗")
|
||||
lines.append("║ 📐 DESIGN VISUALIZATION ║")
|
||||
lines.append(f"║ Project: {project_name:<51} ║")
|
||||
lines.append("╚═══════════════════════════════════════════════════════════════╝")
|
||||
lines.append("")
|
||||
|
||||
# Summary counts
|
||||
lines.append("📊 ENTITY SUMMARY")
|
||||
lines.append("─" * 65)
|
||||
lines.append(f" Pages: {len(pages):>3} │ Components: {len(components):>3} │ API Endpoints: {len(api_endpoints):>3}")
|
||||
lines.append("")
|
||||
|
||||
# Page Flow
|
||||
if pages:
|
||||
lines.extend(visualize_page_flow(pages))
|
||||
lines.append("")
|
||||
|
||||
# Detailed Pages with Components
|
||||
if pages:
|
||||
lines.append("")
|
||||
lines.append("📄 PAGE DETAILS")
|
||||
lines.append("═" * 65)
|
||||
for page in pages:
|
||||
lines.extend(visualize_page(page, components))
|
||||
lines.append("")
|
||||
|
||||
# Component Tree
|
||||
if components:
|
||||
lines.append("")
|
||||
lines.append("🧩 COMPONENT HIERARCHY")
|
||||
lines.append("═" * 65)
|
||||
lines.extend(visualize_component_tree(components))
|
||||
lines.append("")
|
||||
|
||||
# API Endpoints
|
||||
if api_endpoints:
|
||||
lines.append("")
|
||||
lines.append("🔌 API LAYER")
|
||||
lines.append("═" * 65)
|
||||
lines.extend(visualize_api_endpoints(api_endpoints))
|
||||
lines.append("")
|
||||
|
||||
# Data Flow Architecture
|
||||
lines.extend(visualize_data_flow(manifest))
|
||||
lines.append("")
|
||||
|
||||
# Legend
|
||||
lines.append("")
|
||||
lines.append("📋 LEGEND")
|
||||
lines.append("─" * 65)
|
||||
lines.append(" ⏳ PENDING - Designed, awaiting approval")
|
||||
lines.append(" ✅ APPROVED - Approved, ready for implementation")
|
||||
lines.append(" 🔄 IN_PROGRESS - Currently being implemented")
|
||||
lines.append(" 🟢 IMPLEMENTED - Implementation complete")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Visualize design from manifest")
|
||||
parser.add_argument("--manifest", required=True, help="Path to project_manifest.json")
|
||||
parser.add_argument("--format", choices=['full', 'pages', 'components', 'api', 'flow'],
|
||||
default='full', help="Visualization format")
|
||||
args = parser.parse_args()
|
||||
|
||||
manifest = load_manifest(args.manifest)
|
||||
|
||||
if manifest is None:
|
||||
print("❌ Error: Could not load manifest from", args.manifest)
|
||||
return 1
|
||||
|
||||
entities = manifest.get('entities', {})
|
||||
|
||||
if not any([
|
||||
entities.get('pages'),
|
||||
entities.get('components'),
|
||||
entities.get('api_endpoints')
|
||||
]):
|
||||
print("⚠️ No entities found in manifest. Design phase may not be complete.")
|
||||
return 0
|
||||
|
||||
if args.format == 'full':
|
||||
print(generate_full_visualization(manifest))
|
||||
elif args.format == 'pages':
|
||||
pages = entities.get('pages', [])
|
||||
components = entities.get('components', [])
|
||||
for page in pages:
|
||||
print("\n".join(visualize_page(page, components)))
|
||||
elif args.format == 'components':
|
||||
components = entities.get('components', [])
|
||||
print("\n".join(visualize_component_tree(components)))
|
||||
elif args.format == 'api':
|
||||
endpoints = entities.get('api_endpoints', [])
|
||||
print("\n".join(visualize_api_endpoints(endpoints)))
|
||||
elif args.format == 'flow':
|
||||
pages = entities.get('pages', [])
|
||||
print("\n".join(visualize_page_flow(pages)))
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -0,0 +1,651 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Implementation Visualizer for guardrail workflow.
|
||||
|
||||
Generates visual representation of implemented pages and components:
|
||||
- Component tree structure
|
||||
- Props and interfaces
|
||||
- Page layouts
|
||||
- API endpoints
|
||||
- File statistics
|
||||
|
||||
Usage:
|
||||
python3 visualize_implementation.py --manifest project_manifest.json
|
||||
python3 visualize_implementation.py --tasks-dir .workflow/versions/v001/tasks
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
# Try to import yaml
|
||||
try:
|
||||
import yaml
|
||||
HAS_YAML = True
|
||||
except ImportError:
|
||||
HAS_YAML = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class ComponentInfo:
|
||||
"""Parsed component information."""
|
||||
name: str
|
||||
file_path: str
|
||||
props: list[str] = field(default_factory=list)
|
||||
imports: list[str] = field(default_factory=list)
|
||||
exports: list[str] = field(default_factory=list)
|
||||
hooks: list[str] = field(default_factory=list)
|
||||
children: list[str] = field(default_factory=list)
|
||||
lines: int = 0
|
||||
has_types: bool = False
|
||||
status: str = "IMPLEMENTED"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PageInfo:
|
||||
"""Parsed page information."""
|
||||
name: str
|
||||
file_path: str
|
||||
route: str
|
||||
components: list[str] = field(default_factory=list)
|
||||
api_calls: list[str] = field(default_factory=list)
|
||||
lines: int = 0
|
||||
is_client: bool = False
|
||||
is_server: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class APIEndpointInfo:
|
||||
"""Parsed API endpoint information."""
|
||||
name: str
|
||||
file_path: str
|
||||
route: str
|
||||
methods: list[str] = field(default_factory=list)
|
||||
has_auth: bool = False
|
||||
has_validation: bool = False
|
||||
lines: int = 0
|
||||
|
||||
|
||||
def load_yaml(filepath: str) -> dict:
|
||||
"""Load YAML file."""
|
||||
if not os.path.exists(filepath):
|
||||
return {}
|
||||
with open(filepath, 'r') as f:
|
||||
content = f.read()
|
||||
if HAS_YAML:
|
||||
return yaml.safe_load(content) or {}
|
||||
# Basic fallback
|
||||
result = {}
|
||||
for line in content.split('\n'):
|
||||
if ':' in line and not line.startswith(' '):
|
||||
key, _, value = line.partition(':')
|
||||
result[key.strip()] = value.strip()
|
||||
return result
|
||||
|
||||
|
||||
def load_manifest(manifest_path: str) -> dict:
|
||||
"""Load project manifest."""
|
||||
if not os.path.exists(manifest_path):
|
||||
return {}
|
||||
try:
|
||||
with open(manifest_path) as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, IOError):
|
||||
return {}
|
||||
|
||||
|
||||
def parse_typescript_file(file_path: str) -> dict:
|
||||
"""Parse TypeScript/TSX file for component information."""
|
||||
if not os.path.exists(file_path):
|
||||
return {'exists': False}
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
lines = content.split('\n')
|
||||
except (IOError, UnicodeDecodeError):
|
||||
return {'exists': False}
|
||||
|
||||
result = {
|
||||
'exists': True,
|
||||
'lines': len(lines),
|
||||
'imports': [],
|
||||
'exports': [],
|
||||
'props': [],
|
||||
'hooks': [],
|
||||
'components_used': [],
|
||||
'api_calls': [],
|
||||
'is_client': "'use client'" in content or '"use client"' in content,
|
||||
'has_types': 'interface ' in content or 'type ' in content,
|
||||
'methods': [],
|
||||
}
|
||||
|
||||
# Extract imports
|
||||
import_pattern = r"import\s+(?:{[^}]+}|\w+)\s+from\s+['\"]([^'\"]+)['\"]"
|
||||
for match in re.finditer(import_pattern, content):
|
||||
result['imports'].append(match.group(1))
|
||||
|
||||
# Extract exports
|
||||
export_patterns = [
|
||||
r"export\s+(?:default\s+)?(?:function|const|class)\s+(\w+)",
|
||||
r"export\s+{\s*([^}]+)\s*}",
|
||||
]
|
||||
for pattern in export_patterns:
|
||||
for match in re.finditer(pattern, content):
|
||||
exports = match.group(1).split(',')
|
||||
result['exports'].extend([e.strip() for e in exports if e.strip()])
|
||||
|
||||
# Extract props interface
|
||||
props_pattern = r"(?:interface|type)\s+(\w*Props\w*)\s*(?:=|{)"
|
||||
for match in re.finditer(props_pattern, content):
|
||||
result['props'].append(match.group(1))
|
||||
|
||||
# Extract React hooks
|
||||
hooks_pattern = r"\b(use[A-Z]\w+)\s*\("
|
||||
for match in re.finditer(hooks_pattern, content):
|
||||
hook = match.group(1)
|
||||
if hook not in result['hooks']:
|
||||
result['hooks'].append(hook)
|
||||
|
||||
# Extract component usage (JSX)
|
||||
component_pattern = r"<([A-Z]\w+)(?:\s|/|>)"
|
||||
for match in re.finditer(component_pattern, content):
|
||||
comp = match.group(1)
|
||||
if comp not in result['components_used'] and comp not in ['React', 'Fragment']:
|
||||
result['components_used'].append(comp)
|
||||
|
||||
# Extract API calls
|
||||
api_patterns = [
|
||||
r"fetch\s*\(\s*['\"`](/api/[^'\"`]+)['\"`]",
|
||||
r"axios\.\w+\s*\(\s*['\"`](/api/[^'\"`]+)['\"`]",
|
||||
]
|
||||
for pattern in api_patterns:
|
||||
for match in re.finditer(pattern, content):
|
||||
result['api_calls'].append(match.group(1))
|
||||
|
||||
# Extract HTTP methods (for API routes)
|
||||
method_pattern = r"export\s+(?:async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH)"
|
||||
for match in re.finditer(method_pattern, content):
|
||||
result['methods'].append(match.group(1))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_route_from_path(file_path: str) -> str:
|
||||
"""Convert file path to route."""
|
||||
# Handle Next.js App Router
|
||||
if '/app/' in file_path:
|
||||
route = file_path.split('/app/')[-1]
|
||||
route = re.sub(r'/page\.(tsx?|jsx?)$', '', route)
|
||||
route = re.sub(r'/route\.(tsx?|jsx?)$', '', route)
|
||||
route = '/' + route if route else '/'
|
||||
# Handle dynamic routes
|
||||
route = re.sub(r'\[(\w+)\]', r':\1', route)
|
||||
return route
|
||||
|
||||
# Handle Pages Router
|
||||
if '/pages/' in file_path:
|
||||
route = file_path.split('/pages/')[-1]
|
||||
route = re.sub(r'\.(tsx?|jsx?)$', '', route)
|
||||
route = re.sub(r'/index$', '', route)
|
||||
route = '/' + route if route else '/'
|
||||
route = re.sub(r'\[(\w+)\]', r':\1', route)
|
||||
return route
|
||||
|
||||
return file_path
|
||||
|
||||
|
||||
def visualize_component(info: ComponentInfo, indent: str = "") -> list[str]:
|
||||
"""Generate ASCII visualization for a component."""
|
||||
lines = []
|
||||
|
||||
status_icon = {
|
||||
'IMPLEMENTED': '🟢',
|
||||
'PENDING': '⏳',
|
||||
'IN_PROGRESS': '🔄',
|
||||
'ERROR': '❌',
|
||||
}.get(info.status, '○')
|
||||
|
||||
# Component header
|
||||
lines.append(f"{indent}┌{'─' * 60}┐")
|
||||
lines.append(f"{indent}│ {status_icon} COMPONENT: {info.name:<46} │")
|
||||
lines.append(f"{indent}│ 📁 {info.file_path:<52} │")
|
||||
lines.append(f"{indent}│ 📏 {info.lines} lines │"[:63] + "│")
|
||||
|
||||
# Props
|
||||
if info.props:
|
||||
lines.append(f"{indent}├{'─' * 60}┤")
|
||||
lines.append(f"{indent}│ PROPS │")
|
||||
for prop in info.props[:3]:
|
||||
lines.append(f"{indent}│ • {prop:<54} │")
|
||||
|
||||
# Hooks
|
||||
if info.hooks:
|
||||
lines.append(f"{indent}├{'─' * 60}┤")
|
||||
lines.append(f"{indent}│ HOOKS │")
|
||||
hooks_str = ', '.join(info.hooks[:5])
|
||||
if len(hooks_str) > 52:
|
||||
hooks_str = hooks_str[:49] + '...'
|
||||
lines.append(f"{indent}│ {hooks_str:<56} │")
|
||||
|
||||
# Children components
|
||||
if info.children:
|
||||
lines.append(f"{indent}├{'─' * 60}┤")
|
||||
lines.append(f"{indent}│ USES COMPONENTS │")
|
||||
for child in info.children[:5]:
|
||||
lines.append(f"{indent}│ └── {child:<52} │")
|
||||
|
||||
lines.append(f"{indent}└{'─' * 60}┘")
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def visualize_page(info: PageInfo, indent: str = "") -> list[str]:
|
||||
"""Generate ASCII visualization for a page."""
|
||||
lines = []
|
||||
|
||||
client_icon = "🖥️" if info.is_client else "🌐"
|
||||
|
||||
# Page header
|
||||
lines.append(f"{indent}╔{'═' * 62}╗")
|
||||
lines.append(f"{indent}║ {client_icon} PAGE: {info.name:<52} ║")
|
||||
lines.append(f"{indent}║ Route: {info.route:<51} ║")
|
||||
lines.append(f"{indent}║ File: {info.file_path:<51} ║")
|
||||
lines.append(f"{indent}╠{'═' * 62}╣")
|
||||
|
||||
# Components used
|
||||
if info.components:
|
||||
lines.append(f"{indent}║ COMPONENTS USED ║")
|
||||
for comp in info.components[:6]:
|
||||
lines.append(f"{indent}║ ├── {comp:<54} ║")
|
||||
if len(info.components) > 6:
|
||||
lines.append(f"{indent}║ └── ... and {len(info.components) - 6} more ║"[:65] + "║")
|
||||
else:
|
||||
lines.append(f"{indent}║ (No child components detected) ║")
|
||||
|
||||
# API calls
|
||||
if info.api_calls:
|
||||
lines.append(f"{indent}╠{'═' * 62}╣")
|
||||
lines.append(f"{indent}║ API CALLS ║")
|
||||
for api in info.api_calls[:4]:
|
||||
api_short = api[:50] if len(api) <= 50 else api[:47] + '...'
|
||||
lines.append(f"{indent}║ 🔌 {api_short:<55} ║")
|
||||
|
||||
lines.append(f"{indent}╚{'═' * 62}╝")
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def visualize_api_endpoint(info: APIEndpointInfo, indent: str = "") -> list[str]:
|
||||
"""Generate ASCII visualization for an API endpoint."""
|
||||
lines = []
|
||||
|
||||
method_colors = {
|
||||
'GET': '🟢',
|
||||
'POST': '🟡',
|
||||
'PUT': '🟠',
|
||||
'PATCH': '🟠',
|
||||
'DELETE': '🔴',
|
||||
}
|
||||
|
||||
methods_str = ' '.join([f"{method_colors.get(m, '⚪')}{m}" for m in info.methods])
|
||||
|
||||
lines.append(f"{indent}┌{'─' * 60}┐")
|
||||
lines.append(f"{indent}│ 🔌 API: {info.route:<50} │")
|
||||
lines.append(f"{indent}│ Methods: {methods_str:<47} │"[:63] + "│")
|
||||
lines.append(f"{indent}│ File: {info.file_path:<50} │")
|
||||
|
||||
features = []
|
||||
if info.has_auth:
|
||||
features.append("🔐 Auth")
|
||||
if info.has_validation:
|
||||
features.append("✓ Validation")
|
||||
if features:
|
||||
features_str = ' '.join(features)
|
||||
lines.append(f"{indent}│ Features: {features_str:<46} │")
|
||||
|
||||
lines.append(f"{indent}└{'─' * 60}┘")
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def generate_implementation_tree(components: list[ComponentInfo]) -> list[str]:
|
||||
"""Generate a tree view of component hierarchy."""
|
||||
lines = []
|
||||
|
||||
lines.append("")
|
||||
lines.append("🌳 COMPONENT HIERARCHY")
|
||||
lines.append("═" * 65)
|
||||
|
||||
if not components:
|
||||
lines.append(" (No components found)")
|
||||
return lines
|
||||
|
||||
# Group by directory
|
||||
by_dir: dict[str, list[ComponentInfo]] = {}
|
||||
for comp in components:
|
||||
dir_path = str(Path(comp.file_path).parent)
|
||||
if dir_path not in by_dir:
|
||||
by_dir[dir_path] = []
|
||||
by_dir[dir_path].append(comp)
|
||||
|
||||
for dir_path, comps in sorted(by_dir.items()):
|
||||
lines.append(f" 📂 {dir_path}/")
|
||||
for i, comp in enumerate(comps):
|
||||
is_last = i == len(comps) - 1
|
||||
prefix = " └──" if is_last else " ├──"
|
||||
status = "🟢" if comp.status == "IMPLEMENTED" else "⏳"
|
||||
lines.append(f" {prefix} {status} {comp.name}")
|
||||
|
||||
# Show props
|
||||
if comp.props:
|
||||
prop_prefix = " " if is_last else " │ "
|
||||
for prop in comp.props[:2]:
|
||||
lines.append(f"{prop_prefix} 📋 {prop}")
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def generate_stats(
|
||||
pages: list[PageInfo],
|
||||
components: list[ComponentInfo],
|
||||
endpoints: list[APIEndpointInfo]
|
||||
) -> list[str]:
|
||||
"""Generate implementation statistics."""
|
||||
lines = []
|
||||
|
||||
total_lines = sum(p.lines for p in pages) + sum(c.lines for c in components) + sum(e.lines for e in endpoints)
|
||||
client_pages = sum(1 for p in pages if p.is_client)
|
||||
server_pages = len(pages) - client_pages
|
||||
typed_components = sum(1 for c in components if c.has_types)
|
||||
|
||||
lines.append("")
|
||||
lines.append("╔══════════════════════════════════════════════════════════════════╗")
|
||||
lines.append("║ 📊 IMPLEMENTATION STATS ║")
|
||||
lines.append("╠══════════════════════════════════════════════════════════════════╣")
|
||||
lines.append(f"║ Pages: {len(pages):<5} │ Client: {client_pages:<3} │ Server: {server_pages:<3} ║")
|
||||
lines.append(f"║ Components: {len(components):<5} │ Typed: {typed_components:<4} ║")
|
||||
lines.append(f"║ API Endpoints: {len(endpoints):<5} ║")
|
||||
lines.append(f"║ Total Lines: {total_lines:<5} ║")
|
||||
lines.append("╠══════════════════════════════════════════════════════════════════╣")
|
||||
|
||||
# Hooks usage
|
||||
all_hooks = []
|
||||
for comp in components:
|
||||
all_hooks.extend(comp.hooks)
|
||||
hook_counts = {}
|
||||
for hook in all_hooks:
|
||||
hook_counts[hook] = hook_counts.get(hook, 0) + 1
|
||||
|
||||
if hook_counts:
|
||||
lines.append("║ HOOKS USAGE ║")
|
||||
for hook, count in sorted(hook_counts.items(), key=lambda x: -x[1])[:5]:
|
||||
lines.append(f"║ {hook:<20} × {count:<3} ║"[:69] + "║")
|
||||
|
||||
lines.append("╚══════════════════════════════════════════════════════════════════╝")
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def generate_page_flow(pages: list[PageInfo]) -> list[str]:
|
||||
"""Generate page flow visualization."""
|
||||
lines = []
|
||||
|
||||
if not pages:
|
||||
return lines
|
||||
|
||||
lines.append("")
|
||||
lines.append("📱 PAGE STRUCTURE")
|
||||
lines.append("═" * 65)
|
||||
|
||||
# Sort by route
|
||||
sorted_pages = sorted(pages, key=lambda p: p.route)
|
||||
|
||||
for i, page in enumerate(sorted_pages):
|
||||
is_last = i == len(sorted_pages) - 1
|
||||
icon = "🖥️" if page.is_client else "🌐"
|
||||
|
||||
# Page box
|
||||
lines.append(f" ┌{'─' * 50}┐")
|
||||
lines.append(f" │ {icon} {page.route:<47} │")
|
||||
lines.append(f" │ {page.name:<48} │")
|
||||
|
||||
# Components count
|
||||
comp_count = len(page.components)
|
||||
api_count = len(page.api_calls)
|
||||
lines.append(f" │ 🧩 {comp_count} components 🔌 {api_count} API calls │"[:56] + "│")
|
||||
lines.append(f" └{'─' * 50}┘")
|
||||
|
||||
if not is_last:
|
||||
lines.append(" │")
|
||||
lines.append(" ▼")
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def visualize_from_manifest(manifest_path: str) -> str:
|
||||
"""Generate full visualization from manifest."""
|
||||
manifest = load_manifest(manifest_path)
|
||||
|
||||
if not manifest:
|
||||
return "❌ Could not load manifest"
|
||||
|
||||
entities = manifest.get('entities', {})
|
||||
project_name = manifest.get('project', {}).get('name', 'Unknown')
|
||||
|
||||
lines = []
|
||||
|
||||
# Header
|
||||
lines.append("")
|
||||
lines.append("╔═══════════════════════════════════════════════════════════════════╗")
|
||||
lines.append("║ 🏗️ IMPLEMENTATION VISUALIZATION ║")
|
||||
lines.append(f"║ Project: {project_name:<56} ║")
|
||||
lines.append("╚═══════════════════════════════════════════════════════════════════╝")
|
||||
|
||||
pages: list[PageInfo] = []
|
||||
components: list[ComponentInfo] = []
|
||||
endpoints: list[APIEndpointInfo] = []
|
||||
|
||||
# Parse pages
|
||||
for page_data in entities.get('pages', []):
|
||||
file_path = page_data.get('file_path', '')
|
||||
if file_path and os.path.exists(file_path):
|
||||
parsed = parse_typescript_file(file_path)
|
||||
page = PageInfo(
|
||||
name=page_data.get('name', 'Unknown'),
|
||||
file_path=file_path,
|
||||
route=get_route_from_path(file_path),
|
||||
components=parsed.get('components_used', []),
|
||||
api_calls=parsed.get('api_calls', []),
|
||||
lines=parsed.get('lines', 0),
|
||||
is_client=parsed.get('is_client', False),
|
||||
)
|
||||
pages.append(page)
|
||||
|
||||
# Parse components
|
||||
for comp_data in entities.get('components', []):
|
||||
file_path = comp_data.get('file_path', '')
|
||||
if file_path and os.path.exists(file_path):
|
||||
parsed = parse_typescript_file(file_path)
|
||||
comp = ComponentInfo(
|
||||
name=comp_data.get('name', 'Unknown'),
|
||||
file_path=file_path,
|
||||
props=parsed.get('props', []),
|
||||
imports=parsed.get('imports', []),
|
||||
exports=parsed.get('exports', []),
|
||||
hooks=parsed.get('hooks', []),
|
||||
children=parsed.get('components_used', []),
|
||||
lines=parsed.get('lines', 0),
|
||||
has_types=parsed.get('has_types', False),
|
||||
status=comp_data.get('status', 'IMPLEMENTED'),
|
||||
)
|
||||
components.append(comp)
|
||||
|
||||
# Parse API endpoints
|
||||
for api_data in entities.get('api_endpoints', []):
|
||||
file_path = api_data.get('file_path', '')
|
||||
if file_path and os.path.exists(file_path):
|
||||
parsed = parse_typescript_file(file_path)
|
||||
endpoint = APIEndpointInfo(
|
||||
name=api_data.get('name', 'Unknown'),
|
||||
file_path=file_path,
|
||||
route=get_route_from_path(file_path),
|
||||
methods=parsed.get('methods', ['GET']),
|
||||
lines=parsed.get('lines', 0),
|
||||
)
|
||||
endpoints.append(endpoint)
|
||||
|
||||
# Page flow
|
||||
lines.extend(generate_page_flow(pages))
|
||||
|
||||
# Detailed pages
|
||||
if pages:
|
||||
lines.append("")
|
||||
lines.append("📄 PAGE DETAILS")
|
||||
lines.append("═" * 65)
|
||||
for page in pages:
|
||||
lines.extend(visualize_page(page))
|
||||
lines.append("")
|
||||
|
||||
# Component hierarchy
|
||||
lines.extend(generate_implementation_tree(components))
|
||||
|
||||
# Detailed components
|
||||
if components:
|
||||
lines.append("")
|
||||
lines.append("🧩 COMPONENT DETAILS")
|
||||
lines.append("═" * 65)
|
||||
for comp in components[:10]: # Limit to 10
|
||||
lines.extend(visualize_component(comp))
|
||||
lines.append("")
|
||||
if len(components) > 10:
|
||||
lines.append(f" ... and {len(components) - 10} more components")
|
||||
|
||||
# API endpoints
|
||||
if endpoints:
|
||||
lines.append("")
|
||||
lines.append("🔌 API ENDPOINTS")
|
||||
lines.append("═" * 65)
|
||||
for endpoint in endpoints:
|
||||
lines.extend(visualize_api_endpoint(endpoint))
|
||||
lines.append("")
|
||||
|
||||
# Stats
|
||||
lines.extend(generate_stats(pages, components, endpoints))
|
||||
|
||||
# Legend
|
||||
lines.append("")
|
||||
lines.append("📋 LEGEND")
|
||||
lines.append("─" * 65)
|
||||
lines.append(" 🟢 Implemented ⏳ Pending 🔄 In Progress ❌ Error")
|
||||
lines.append(" 🖥️ Client Component 🌐 Server Component")
|
||||
lines.append(" 🟢 GET 🟡 POST 🟠 PUT/PATCH 🔴 DELETE")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def visualize_from_tasks(tasks_dir: str) -> str:
|
||||
"""Generate visualization from task files."""
|
||||
tasks_path = Path(tasks_dir)
|
||||
|
||||
if not tasks_path.exists():
|
||||
return f"❌ Tasks directory not found: {tasks_dir}"
|
||||
|
||||
task_files = list(tasks_path.glob('task_*.yml'))
|
||||
|
||||
if not task_files:
|
||||
return f"❌ No task files found in: {tasks_dir}"
|
||||
|
||||
lines = []
|
||||
lines.append("")
|
||||
lines.append("╔═══════════════════════════════════════════════════════════════════╗")
|
||||
lines.append("║ 📋 TASK IMPLEMENTATION STATUS ║")
|
||||
lines.append("╚═══════════════════════════════════════════════════════════════════╝")
|
||||
lines.append("")
|
||||
|
||||
implemented_files = []
|
||||
|
||||
for task_file in sorted(task_files):
|
||||
task = load_yaml(str(task_file))
|
||||
task_id = task.get('id', task_file.stem)
|
||||
status = task.get('status', 'unknown')
|
||||
title = task.get('title', 'Unknown task')
|
||||
file_paths = task.get('file_paths', [])
|
||||
|
||||
status_icon = {
|
||||
'completed': '✅',
|
||||
'approved': '✅',
|
||||
'pending': '⏳',
|
||||
'in_progress': '🔄',
|
||||
'blocked': '🚫',
|
||||
}.get(status, '○')
|
||||
|
||||
lines.append(f" {status_icon} {task_id}")
|
||||
lines.append(f" {title[:55]}")
|
||||
|
||||
for fp in file_paths:
|
||||
if os.path.exists(fp):
|
||||
lines.append(f" └── ✓ {fp}")
|
||||
implemented_files.append(fp)
|
||||
else:
|
||||
lines.append(f" └── ✗ {fp} (missing)")
|
||||
|
||||
lines.append("")
|
||||
|
||||
# Parse and visualize implemented files
|
||||
if implemented_files:
|
||||
lines.append("─" * 65)
|
||||
lines.append("")
|
||||
lines.append("🔍 IMPLEMENTED FILES ANALYSIS")
|
||||
lines.append("")
|
||||
|
||||
for fp in implemented_files[:5]:
|
||||
parsed = parse_typescript_file(fp)
|
||||
if parsed.get('exists'):
|
||||
name = Path(fp).stem
|
||||
lines.append(f" 📁 {fp}")
|
||||
lines.append(f" Lines: {parsed.get('lines', 0)}")
|
||||
|
||||
if parsed.get('exports'):
|
||||
lines.append(f" Exports: {', '.join(parsed['exports'][:3])}")
|
||||
if parsed.get('hooks'):
|
||||
lines.append(f" Hooks: {', '.join(parsed['hooks'][:3])}")
|
||||
if parsed.get('components_used'):
|
||||
lines.append(f" Uses: {', '.join(parsed['components_used'][:3])}")
|
||||
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Visualize implementation")
|
||||
parser.add_argument('--manifest', help='Path to project_manifest.json')
|
||||
parser.add_argument('--tasks-dir', help='Path to tasks directory')
|
||||
parser.add_argument('--format', choices=['full', 'tree', 'stats', 'pages'],
|
||||
default='full', help='Output format')
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.manifest:
|
||||
output = visualize_from_manifest(args.manifest)
|
||||
elif args.tasks_dir:
|
||||
output = visualize_from_tasks(args.tasks_dir)
|
||||
else:
|
||||
# Auto-detect
|
||||
if os.path.exists('project_manifest.json'):
|
||||
output = visualize_from_manifest('project_manifest.json')
|
||||
else:
|
||||
output = "Usage: python3 visualize_implementation.py --manifest project_manifest.json"
|
||||
|
||||
print(output)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -0,0 +1,835 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Workflow state management for automated orchestration with approval gates."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# Try to import yaml, fall back to basic parsing if not available
|
||||
try:
|
||||
import yaml
|
||||
HAS_YAML = True
|
||||
except ImportError:
|
||||
HAS_YAML = False
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# YAML Helpers
|
||||
# ============================================================================
|
||||
|
||||
def load_yaml(filepath: str) -> dict:
|
||||
"""Load YAML file."""
|
||||
if not os.path.exists(filepath):
|
||||
return {}
|
||||
with open(filepath, 'r') as f:
|
||||
content = f.read()
|
||||
if not content.strip():
|
||||
return {}
|
||||
|
||||
if HAS_YAML:
|
||||
return yaml.safe_load(content) or {}
|
||||
|
||||
# Simple fallback parser
|
||||
result = {}
|
||||
current_key = None
|
||||
current_list = None
|
||||
|
||||
for line in content.split('\n'):
|
||||
line = line.rstrip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
|
||||
if line.startswith(' - '):
|
||||
if current_list is not None:
|
||||
value = line[4:].strip()
|
||||
# Handle quoted strings
|
||||
if (value.startswith('"') and value.endswith('"')) or \
|
||||
(value.startswith("'") and value.endswith("'")):
|
||||
value = value[1:-1]
|
||||
current_list.append(value)
|
||||
continue
|
||||
|
||||
if ':' in line and not line.startswith(' '):
|
||||
key, _, value = line.partition(':')
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
|
||||
if value == '[]':
|
||||
result[key] = []
|
||||
current_list = result[key]
|
||||
elif value == '{}':
|
||||
result[key] = {}
|
||||
current_list = None
|
||||
elif value == 'null' or value == '~':
|
||||
result[key] = None
|
||||
current_list = None
|
||||
elif value == 'true':
|
||||
result[key] = True
|
||||
current_list = None
|
||||
elif value == 'false':
|
||||
result[key] = False
|
||||
current_list = None
|
||||
elif value.isdigit():
|
||||
result[key] = int(value)
|
||||
current_list = None
|
||||
elif value:
|
||||
# Handle quoted strings
|
||||
if (value.startswith('"') and value.endswith('"')) or \
|
||||
(value.startswith("'") and value.endswith("'")):
|
||||
value = value[1:-1]
|
||||
result[key] = value
|
||||
current_list = None
|
||||
else:
|
||||
result[key] = []
|
||||
current_list = result[key]
|
||||
current_key = key
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def save_yaml(filepath: str, data: dict):
|
||||
"""Save data to YAML file."""
|
||||
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||
|
||||
if HAS_YAML:
|
||||
with open(filepath, 'w') as f:
|
||||
yaml.dump(data, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
||||
else:
|
||||
# Simple YAML writer
|
||||
def write_value(value, indent=0):
|
||||
prefix = ' ' * indent
|
||||
lines = []
|
||||
if isinstance(value, dict):
|
||||
for k, v in value.items():
|
||||
if isinstance(v, (dict, list)) and v:
|
||||
lines.append(f"{prefix}{k}:")
|
||||
lines.extend(write_value(v, indent + 1))
|
||||
elif isinstance(v, list):
|
||||
lines.append(f"{prefix}{k}: []")
|
||||
else:
|
||||
lines.append(f"{prefix}{k}: {v}")
|
||||
elif isinstance(value, list):
|
||||
for item in value:
|
||||
if isinstance(item, dict):
|
||||
lines.append(f"{prefix}-")
|
||||
for k, v in item.items():
|
||||
lines.append(f"{prefix} {k}: {v}")
|
||||
else:
|
||||
lines.append(f"{prefix}- {item}")
|
||||
return lines
|
||||
|
||||
lines = write_value(data)
|
||||
with open(filepath, 'w') as f:
|
||||
f.write('\n'.join(lines))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Workflow State Management
|
||||
# ============================================================================
|
||||
|
||||
PHASES = [
|
||||
'INITIALIZING',
|
||||
'DESIGNING',
|
||||
'AWAITING_DESIGN_APPROVAL',
|
||||
'DESIGN_APPROVED',
|
||||
'DESIGN_REJECTED',
|
||||
'IMPLEMENTING',
|
||||
'REVIEWING',
|
||||
'SECURITY_REVIEW', # New phase for security audit
|
||||
'AWAITING_IMPL_APPROVAL',
|
||||
'IMPL_APPROVED',
|
||||
'IMPL_REJECTED',
|
||||
'COMPLETING',
|
||||
'COMPLETED',
|
||||
'PAUSED',
|
||||
'FAILED'
|
||||
]
|
||||
|
||||
VALID_TRANSITIONS = {
|
||||
'INITIALIZING': ['DESIGNING', 'FAILED'],
|
||||
'DESIGNING': ['AWAITING_DESIGN_APPROVAL', 'FAILED'],
|
||||
'AWAITING_DESIGN_APPROVAL': ['DESIGN_APPROVED', 'DESIGN_REJECTED', 'PAUSED'],
|
||||
'DESIGN_APPROVED': ['IMPLEMENTING', 'FAILED'],
|
||||
'DESIGN_REJECTED': ['DESIGNING'],
|
||||
'IMPLEMENTING': ['REVIEWING', 'FAILED', 'PAUSED'],
|
||||
'REVIEWING': ['SECURITY_REVIEW', 'IMPLEMENTING', 'FAILED'], # Must pass through security
|
||||
'SECURITY_REVIEW': ['AWAITING_IMPL_APPROVAL', 'IMPLEMENTING', 'FAILED'], # Can go back to fix
|
||||
'AWAITING_IMPL_APPROVAL': ['IMPL_APPROVED', 'IMPL_REJECTED', 'PAUSED'],
|
||||
'IMPL_APPROVED': ['COMPLETING', 'FAILED'],
|
||||
'IMPL_REJECTED': ['IMPLEMENTING'],
|
||||
'COMPLETING': ['COMPLETED', 'FAILED'],
|
||||
'COMPLETED': [],
|
||||
'PAUSED': PHASES, # Can resume to any phase
|
||||
'FAILED': ['INITIALIZING', 'DESIGNING', 'IMPLEMENTING'] # Can retry
|
||||
}
|
||||
|
||||
|
||||
def get_workflow_dir() -> Path:
|
||||
"""Get the .workflow directory path."""
|
||||
return Path('.workflow')
|
||||
|
||||
|
||||
def get_current_state_path() -> Path:
|
||||
"""Get the current workflow state file path."""
|
||||
return get_workflow_dir() / 'current.yml'
|
||||
|
||||
|
||||
def get_history_dir() -> Path:
|
||||
"""Get the workflow history directory."""
|
||||
return get_workflow_dir() / 'history'
|
||||
|
||||
|
||||
def create_workflow(feature: str) -> dict:
|
||||
"""Create a new workflow state."""
|
||||
now = datetime.now()
|
||||
workflow_id = f"workflow_{now.strftime('%Y%m%d_%H%M%S')}"
|
||||
|
||||
state = {
|
||||
'id': workflow_id,
|
||||
'feature': feature,
|
||||
'current_phase': 'INITIALIZING',
|
||||
'gates': {
|
||||
'design_approval': {
|
||||
'status': 'pending',
|
||||
'approved_at': None,
|
||||
'approved_by': None,
|
||||
'rejection_reason': None,
|
||||
'revision_count': 0
|
||||
},
|
||||
'implementation_approval': {
|
||||
'status': 'pending',
|
||||
'approved_at': None,
|
||||
'approved_by': None,
|
||||
'rejection_reason': None,
|
||||
'revision_count': 0
|
||||
}
|
||||
},
|
||||
'progress': {
|
||||
'entities_designed': 0,
|
||||
'tasks_created': 0,
|
||||
'tasks_implemented': 0,
|
||||
'tasks_reviewed': 0,
|
||||
'tasks_approved': 0,
|
||||
'tasks_completed': 0
|
||||
},
|
||||
'tasks': {
|
||||
'pending': [],
|
||||
'in_progress': [],
|
||||
'review': [],
|
||||
'approved': [],
|
||||
'completed': [],
|
||||
'blocked': []
|
||||
},
|
||||
'started_at': now.isoformat(),
|
||||
'updated_at': now.isoformat(),
|
||||
'completed_at': None,
|
||||
'last_error': None,
|
||||
'resume_point': {
|
||||
'phase': 'INITIALIZING',
|
||||
'task_id': None,
|
||||
'action': 'start_workflow'
|
||||
},
|
||||
'checkpoints': [] # List of checkpoint snapshots for recovery
|
||||
}
|
||||
|
||||
# Ensure directory exists
|
||||
get_workflow_dir().mkdir(exist_ok=True)
|
||||
get_history_dir().mkdir(exist_ok=True)
|
||||
|
||||
# Save state
|
||||
save_yaml(str(get_current_state_path()), state)
|
||||
|
||||
return state
|
||||
|
||||
|
||||
def load_current_workflow() -> Optional[dict]:
|
||||
"""Load the current workflow state from the active version."""
|
||||
state_path = get_current_state_path()
|
||||
if not state_path.exists():
|
||||
return None
|
||||
|
||||
# Read current.yml to get active version
|
||||
current = load_yaml(str(state_path))
|
||||
active_version = current.get('active_version')
|
||||
if not active_version:
|
||||
return None
|
||||
|
||||
# Load the version's session.yml
|
||||
version_session_path = get_workflow_dir() / 'versions' / active_version / 'session.yml'
|
||||
if not version_session_path.exists():
|
||||
return None
|
||||
|
||||
session = load_yaml(str(version_session_path))
|
||||
|
||||
current_phase = session.get('current_phase', 'INITIALIZING')
|
||||
|
||||
# Convert session format to state format expected by show_status
|
||||
return {
|
||||
'id': session.get('session_id', active_version),
|
||||
'feature': session.get('feature', 'Unknown'),
|
||||
'current_phase': current_phase,
|
||||
'gates': {
|
||||
'design_approval': session.get('approvals', {}).get('design', {'status': 'pending'}),
|
||||
'implementation_approval': session.get('approvals', {}).get('implementation', {'status': 'pending'})
|
||||
},
|
||||
'progress': {
|
||||
'entities_designed': session.get('summary', {}).get('entities_created', 0),
|
||||
'tasks_created': session.get('summary', {}).get('total_tasks', 0),
|
||||
'tasks_implemented': session.get('summary', {}).get('tasks_completed', 0),
|
||||
'tasks_reviewed': 0,
|
||||
'tasks_completed': session.get('summary', {}).get('tasks_completed', 0)
|
||||
},
|
||||
'tasks': {
|
||||
'pending': [],
|
||||
'in_progress': [],
|
||||
'review': [],
|
||||
'approved': [],
|
||||
'completed': session.get('task_sessions', []),
|
||||
'blocked': []
|
||||
},
|
||||
'version': active_version,
|
||||
'status': session.get('status', 'unknown'),
|
||||
'last_error': None,
|
||||
'started_at': session.get('started_at', ''),
|
||||
'updated_at': session.get('updated_at', ''),
|
||||
'completed_at': session.get('completed_at'),
|
||||
'resume_point': {
|
||||
'phase': current_phase,
|
||||
'task_id': None,
|
||||
'action': 'continue_workflow'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def save_workflow(state: dict):
|
||||
"""Save workflow state to the version's session.yml file."""
|
||||
# Get active version
|
||||
current_path = get_current_state_path()
|
||||
if not current_path.exists():
|
||||
print("Error: No current.yml found")
|
||||
return
|
||||
|
||||
current = load_yaml(str(current_path))
|
||||
active_version = current.get('active_version')
|
||||
if not active_version:
|
||||
print("Error: No active version set")
|
||||
return
|
||||
|
||||
# Get the version's session.yml path
|
||||
version_session_path = get_workflow_dir() / 'versions' / active_version / 'session.yml'
|
||||
if not version_session_path.exists():
|
||||
print(f"Error: Session file not found: {version_session_path}")
|
||||
return
|
||||
|
||||
# Load existing session data
|
||||
session = load_yaml(str(version_session_path))
|
||||
|
||||
# Create backup
|
||||
backup_path = version_session_path.with_suffix('.yml.bak')
|
||||
shutil.copy(version_session_path, backup_path)
|
||||
|
||||
# Update session with state changes
|
||||
session['current_phase'] = state['current_phase']
|
||||
session['updated_at'] = datetime.now().isoformat()
|
||||
|
||||
if state.get('completed_at'):
|
||||
session['completed_at'] = state['completed_at']
|
||||
session['status'] = 'completed'
|
||||
|
||||
# Update approvals
|
||||
if 'gates' in state:
|
||||
if 'approvals' not in session:
|
||||
session['approvals'] = {}
|
||||
if state['gates'].get('design_approval', {}).get('status') == 'approved':
|
||||
session['approvals']['design'] = state['gates']['design_approval']
|
||||
if state['gates'].get('implementation_approval', {}).get('status') == 'approved':
|
||||
session['approvals']['implementation'] = state['gates']['implementation_approval']
|
||||
|
||||
save_yaml(str(version_session_path), session)
|
||||
|
||||
|
||||
def transition_phase(state: dict, new_phase: str, error: str = None) -> bool:
|
||||
"""Transition workflow to a new phase."""
|
||||
current = state['current_phase']
|
||||
|
||||
if new_phase not in PHASES:
|
||||
print(f"Error: Invalid phase '{new_phase}'")
|
||||
return False
|
||||
|
||||
if new_phase not in VALID_TRANSITIONS.get(current, []):
|
||||
print(f"Error: Cannot transition from '{current}' to '{new_phase}'")
|
||||
print(f"Valid transitions: {VALID_TRANSITIONS.get(current, [])}")
|
||||
return False
|
||||
|
||||
state['current_phase'] = new_phase
|
||||
state['resume_point']['phase'] = new_phase
|
||||
|
||||
if new_phase == 'FAILED' and error:
|
||||
state['last_error'] = error
|
||||
|
||||
if new_phase == 'COMPLETED':
|
||||
state['completed_at'] = datetime.now().isoformat()
|
||||
|
||||
# Set appropriate resume action
|
||||
resume_actions = {
|
||||
'INITIALIZING': 'start_workflow',
|
||||
'DESIGNING': 'continue_design',
|
||||
'AWAITING_DESIGN_APPROVAL': 'await_user_approval',
|
||||
'DESIGN_APPROVED': 'start_implementation',
|
||||
'DESIGN_REJECTED': 'revise_design',
|
||||
'IMPLEMENTING': 'continue_implementation',
|
||||
'REVIEWING': 'continue_review',
|
||||
'SECURITY_REVIEW': 'run_security_audit',
|
||||
'AWAITING_IMPL_APPROVAL': 'await_user_approval',
|
||||
'IMPL_APPROVED': 'start_completion',
|
||||
'IMPL_REJECTED': 'fix_implementation',
|
||||
'COMPLETING': 'continue_completion',
|
||||
'COMPLETED': 'workflow_done',
|
||||
'PAUSED': 'resume_workflow',
|
||||
'FAILED': 'retry_or_abort'
|
||||
}
|
||||
state['resume_point']['action'] = resume_actions.get(new_phase, 'unknown')
|
||||
|
||||
save_workflow(state)
|
||||
return True
|
||||
|
||||
|
||||
def approve_gate(state: dict, gate: str, approver: str = 'user') -> bool:
|
||||
"""Approve a gate."""
|
||||
if gate not in ['design_approval', 'implementation_approval']:
|
||||
print(f"Error: Invalid gate '{gate}'")
|
||||
return False
|
||||
|
||||
state['gates'][gate]['status'] = 'approved'
|
||||
state['gates'][gate]['approved_at'] = datetime.now().isoformat()
|
||||
state['gates'][gate]['approved_by'] = approver
|
||||
|
||||
# Transition to next phase
|
||||
if gate == 'design_approval':
|
||||
transition_phase(state, 'DESIGN_APPROVED')
|
||||
else:
|
||||
transition_phase(state, 'IMPL_APPROVED')
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def reject_gate(state: dict, gate: str, reason: str) -> bool:
|
||||
"""Reject a gate."""
|
||||
if gate not in ['design_approval', 'implementation_approval']:
|
||||
print(f"Error: Invalid gate '{gate}'")
|
||||
return False
|
||||
|
||||
state['gates'][gate]['status'] = 'rejected'
|
||||
state['gates'][gate]['rejection_reason'] = reason
|
||||
state['gates'][gate]['revision_count'] += 1
|
||||
|
||||
# Transition to rejection phase
|
||||
if gate == 'design_approval':
|
||||
transition_phase(state, 'DESIGN_REJECTED')
|
||||
else:
|
||||
transition_phase(state, 'IMPL_REJECTED')
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def update_progress(state: dict, **kwargs):
|
||||
"""Update progress counters."""
|
||||
for key, value in kwargs.items():
|
||||
if key in state['progress']:
|
||||
state['progress'][key] = value
|
||||
save_workflow(state)
|
||||
|
||||
|
||||
def update_task_status(state: dict, task_id: str, new_status: str):
|
||||
"""Update task status in workflow state."""
|
||||
# Remove from all status lists
|
||||
for status in state['tasks']:
|
||||
if task_id in state['tasks'][status]:
|
||||
state['tasks'][status].remove(task_id)
|
||||
|
||||
# Add to new status list
|
||||
if new_status in state['tasks']:
|
||||
state['tasks'][new_status].append(task_id)
|
||||
|
||||
# Update resume point if task is in progress
|
||||
if new_status == 'in_progress':
|
||||
state['resume_point']['task_id'] = task_id
|
||||
|
||||
save_workflow(state)
|
||||
|
||||
|
||||
def save_checkpoint(state: dict, description: str, data: dict = None) -> dict:
|
||||
"""Save a checkpoint for recovery during long operations.
|
||||
|
||||
Args:
|
||||
state: Current workflow state
|
||||
description: Human-readable description of checkpoint
|
||||
data: Optional additional data to store
|
||||
|
||||
Returns:
|
||||
The checkpoint object that was created
|
||||
"""
|
||||
checkpoint = {
|
||||
'id': f"checkpoint_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'phase': state['current_phase'],
|
||||
'description': description,
|
||||
'resume_point': state['resume_point'].copy(),
|
||||
'progress': state['progress'].copy(),
|
||||
'data': data or {}
|
||||
}
|
||||
|
||||
# Keep only last 10 checkpoints to avoid bloat
|
||||
if 'checkpoints' not in state:
|
||||
state['checkpoints'] = []
|
||||
state['checkpoints'].append(checkpoint)
|
||||
if len(state['checkpoints']) > 10:
|
||||
state['checkpoints'] = state['checkpoints'][-10:]
|
||||
|
||||
save_workflow(state)
|
||||
return checkpoint
|
||||
|
||||
|
||||
def get_latest_checkpoint(state: dict) -> Optional[dict]:
|
||||
"""Get the most recent checkpoint.
|
||||
|
||||
Returns:
|
||||
Latest checkpoint or None if no checkpoints exist
|
||||
"""
|
||||
checkpoints = state.get('checkpoints', [])
|
||||
return checkpoints[-1] if checkpoints else None
|
||||
|
||||
|
||||
def restore_from_checkpoint(state: dict, checkpoint_id: str = None) -> bool:
|
||||
"""Restore workflow state from a checkpoint.
|
||||
|
||||
Args:
|
||||
state: Current workflow state
|
||||
checkpoint_id: Optional specific checkpoint ID, defaults to latest
|
||||
|
||||
Returns:
|
||||
True if restoration was successful
|
||||
"""
|
||||
checkpoints = state.get('checkpoints', [])
|
||||
if not checkpoints:
|
||||
print("Error: No checkpoints available")
|
||||
return False
|
||||
|
||||
# Find checkpoint
|
||||
if checkpoint_id:
|
||||
checkpoint = next((c for c in checkpoints if c['id'] == checkpoint_id), None)
|
||||
if not checkpoint:
|
||||
print(f"Error: Checkpoint '{checkpoint_id}' not found")
|
||||
return False
|
||||
else:
|
||||
checkpoint = checkpoints[-1]
|
||||
|
||||
# Restore state from checkpoint
|
||||
state['resume_point'] = checkpoint['resume_point'].copy()
|
||||
state['progress'] = checkpoint['progress'].copy()
|
||||
state['current_phase'] = checkpoint['phase']
|
||||
state['last_error'] = None # Clear any error since we're recovering
|
||||
|
||||
save_workflow(state)
|
||||
print(f"Restored from checkpoint: {checkpoint['description']}")
|
||||
return True
|
||||
|
||||
|
||||
def list_checkpoints(state: dict) -> list:
|
||||
"""List all available checkpoints.
|
||||
|
||||
Returns:
|
||||
List of checkpoint summaries
|
||||
"""
|
||||
return [
|
||||
{
|
||||
'id': c['id'],
|
||||
'timestamp': c['timestamp'],
|
||||
'phase': c['phase'],
|
||||
'description': c['description']
|
||||
}
|
||||
for c in state.get('checkpoints', [])
|
||||
]
|
||||
|
||||
|
||||
def clear_checkpoints(state: dict):
|
||||
"""Clear all checkpoints (typically after successful completion)."""
|
||||
state['checkpoints'] = []
|
||||
save_workflow(state)
|
||||
|
||||
|
||||
def archive_workflow(state: dict, suffix: str = ''):
|
||||
"""Archive completed/aborted workflow."""
|
||||
history_dir = get_history_dir()
|
||||
history_dir.mkdir(exist_ok=True)
|
||||
|
||||
filename = f"{state['id']}{suffix}.yml"
|
||||
archive_path = history_dir / filename
|
||||
|
||||
save_yaml(str(archive_path), state)
|
||||
|
||||
# Remove current state
|
||||
current_path = get_current_state_path()
|
||||
if current_path.exists():
|
||||
current_path.unlink()
|
||||
|
||||
|
||||
def show_status(state: dict):
|
||||
"""Display workflow status."""
|
||||
print()
|
||||
print("╔" + "═" * 58 + "╗")
|
||||
print("║" + "WORKFLOW STATUS".center(58) + "║")
|
||||
print("╠" + "═" * 58 + "╣")
|
||||
print("║" + f" ID: {state['id']}".ljust(58) + "║")
|
||||
print("║" + f" Feature: {state['feature'][:45]}".ljust(58) + "║")
|
||||
print("║" + f" Phase: {state['current_phase']}".ljust(58) + "║")
|
||||
print("╠" + "═" * 58 + "╣")
|
||||
print("║" + " APPROVAL GATES".ljust(58) + "║")
|
||||
|
||||
design_gate = state['gates']['design_approval']
|
||||
impl_gate = state['gates']['implementation_approval']
|
||||
|
||||
design_icon = "✅" if design_gate['status'] == 'approved' else "❌" if design_gate['status'] == 'rejected' else "⏳"
|
||||
impl_icon = "✅" if impl_gate['status'] == 'approved' else "❌" if impl_gate['status'] == 'rejected' else "⏳"
|
||||
|
||||
print("║" + f" {design_icon} Design: {design_gate['status']}".ljust(58) + "║")
|
||||
print("║" + f" {impl_icon} Implementation: {impl_gate['status']}".ljust(58) + "║")
|
||||
print("╠" + "═" * 58 + "╣")
|
||||
print("║" + " PROGRESS".ljust(58) + "║")
|
||||
|
||||
p = state['progress']
|
||||
print("║" + f" Entities Designed: {p['entities_designed']}".ljust(58) + "║")
|
||||
print("║" + f" Tasks Created: {p['tasks_created']}".ljust(58) + "║")
|
||||
print("║" + f" Tasks Implemented: {p['tasks_implemented']}".ljust(58) + "║")
|
||||
print("║" + f" Tasks Reviewed: {p['tasks_reviewed']}".ljust(58) + "║")
|
||||
print("║" + f" Tasks Completed: {p['tasks_completed']}".ljust(58) + "║")
|
||||
print("╠" + "═" * 58 + "╣")
|
||||
print("║" + " TASK BREAKDOWN".ljust(58) + "║")
|
||||
|
||||
t = state['tasks']
|
||||
print("║" + f" ⏳ Pending: {len(t['pending'])}".ljust(58) + "║")
|
||||
print("║" + f" 🔄 In Progress: {len(t['in_progress'])}".ljust(58) + "║")
|
||||
print("║" + f" 🔍 Review: {len(t['review'])}".ljust(58) + "║")
|
||||
print("║" + f" ✅ Approved: {len(t['approved'])}".ljust(58) + "║")
|
||||
print("║" + f" ✓ Completed: {len(t['completed'])}".ljust(58) + "║")
|
||||
print("║" + f" 🚫 Blocked: {len(t['blocked'])}".ljust(58) + "║")
|
||||
|
||||
if state['last_error']:
|
||||
print("╠" + "═" * 58 + "╣")
|
||||
print("║" + " ⚠️ LAST ERROR".ljust(58) + "║")
|
||||
print("║" + f" {state['last_error'][:52]}".ljust(58) + "║")
|
||||
|
||||
print("╠" + "═" * 58 + "╣")
|
||||
print("║" + " TIMESTAMPS".ljust(58) + "║")
|
||||
print("║" + f" Started: {state['started_at'][:19]}".ljust(58) + "║")
|
||||
print("║" + f" Updated: {state['updated_at'][:19]}".ljust(58) + "║")
|
||||
if state['completed_at']:
|
||||
print("║" + f" Completed: {state['completed_at'][:19]}".ljust(58) + "║")
|
||||
print("╚" + "═" * 58 + "╝")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CLI Interface
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Workflow state management")
|
||||
subparsers = parser.add_subparsers(dest='command', help='Commands')
|
||||
|
||||
# create command
|
||||
create_parser = subparsers.add_parser('create', help='Create new workflow')
|
||||
create_parser.add_argument('feature', help='Feature to implement')
|
||||
|
||||
# status command
|
||||
subparsers.add_parser('status', help='Show workflow status')
|
||||
|
||||
# transition command
|
||||
trans_parser = subparsers.add_parser('transition', help='Transition to new phase')
|
||||
trans_parser.add_argument('phase', choices=PHASES, help='Target phase')
|
||||
trans_parser.add_argument('--error', help='Error message (for FAILED phase)')
|
||||
|
||||
# approve command
|
||||
approve_parser = subparsers.add_parser('approve', help='Approve a gate')
|
||||
approve_parser.add_argument('gate', choices=['design', 'implementation'], help='Gate to approve')
|
||||
approve_parser.add_argument('--approver', default='user', help='Approver name')
|
||||
|
||||
# reject command
|
||||
reject_parser = subparsers.add_parser('reject', help='Reject a gate')
|
||||
reject_parser.add_argument('gate', choices=['design', 'implementation'], help='Gate to reject')
|
||||
reject_parser.add_argument('reason', help='Rejection reason')
|
||||
|
||||
# progress command
|
||||
progress_parser = subparsers.add_parser('progress', help='Update progress')
|
||||
progress_parser.add_argument('--entities', type=int, help='Entities designed')
|
||||
progress_parser.add_argument('--tasks-created', type=int, help='Tasks created')
|
||||
progress_parser.add_argument('--tasks-impl', type=int, help='Tasks implemented')
|
||||
progress_parser.add_argument('--tasks-reviewed', type=int, help='Tasks reviewed')
|
||||
progress_parser.add_argument('--tasks-completed', type=int, help='Tasks completed')
|
||||
|
||||
# task command
|
||||
task_parser = subparsers.add_parser('task', help='Update task status')
|
||||
task_parser.add_argument('task_id', help='Task ID')
|
||||
task_parser.add_argument('status', choices=['pending', 'in_progress', 'review', 'approved', 'completed', 'blocked'])
|
||||
|
||||
# archive command
|
||||
archive_parser = subparsers.add_parser('archive', help='Archive workflow')
|
||||
archive_parser.add_argument('--suffix', default='', help='Filename suffix (e.g., _aborted)')
|
||||
|
||||
# exists command
|
||||
subparsers.add_parser('exists', help='Check if workflow exists')
|
||||
|
||||
# checkpoint command
|
||||
checkpoint_parser = subparsers.add_parser('checkpoint', help='Manage checkpoints')
|
||||
checkpoint_parser.add_argument('action', choices=['save', 'list', 'restore', 'clear'],
|
||||
help='Checkpoint action')
|
||||
checkpoint_parser.add_argument('--description', '-d', help='Checkpoint description (for save)')
|
||||
checkpoint_parser.add_argument('--id', help='Checkpoint ID (for restore)')
|
||||
checkpoint_parser.add_argument('--data', help='JSON data to store (for save)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == 'create':
|
||||
state = create_workflow(args.feature)
|
||||
print(f"Created workflow: {state['id']}")
|
||||
print(f"Feature: {args.feature}")
|
||||
print(f"State saved to: {get_current_state_path()}")
|
||||
|
||||
elif args.command == 'status':
|
||||
state = load_current_workflow()
|
||||
if state:
|
||||
show_status(state)
|
||||
else:
|
||||
print("No active workflow found.")
|
||||
print("Start a new workflow with: /workflow:spawn <feature>")
|
||||
|
||||
elif args.command == 'transition':
|
||||
state = load_current_workflow()
|
||||
if not state:
|
||||
print("Error: No active workflow")
|
||||
sys.exit(1)
|
||||
if transition_phase(state, args.phase, args.error):
|
||||
print(f"Transitioned to: {args.phase}")
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
elif args.command == 'approve':
|
||||
state = load_current_workflow()
|
||||
if not state:
|
||||
print("Error: No active workflow")
|
||||
sys.exit(1)
|
||||
gate = f"{args.gate}_approval"
|
||||
if approve_gate(state, gate, args.approver):
|
||||
print(f"Approved: {args.gate}")
|
||||
|
||||
elif args.command == 'reject':
|
||||
state = load_current_workflow()
|
||||
if not state:
|
||||
print("Error: No active workflow")
|
||||
sys.exit(1)
|
||||
gate = f"{args.gate}_approval"
|
||||
if reject_gate(state, gate, args.reason):
|
||||
print(f"Rejected: {args.gate}")
|
||||
print(f"Reason: {args.reason}")
|
||||
|
||||
elif args.command == 'progress':
|
||||
state = load_current_workflow()
|
||||
if not state:
|
||||
print("Error: No active workflow")
|
||||
sys.exit(1)
|
||||
updates = {}
|
||||
if args.entities is not None:
|
||||
updates['entities_designed'] = args.entities
|
||||
if args.tasks_created is not None:
|
||||
updates['tasks_created'] = args.tasks_created
|
||||
if args.tasks_impl is not None:
|
||||
updates['tasks_implemented'] = args.tasks_impl
|
||||
if args.tasks_reviewed is not None:
|
||||
updates['tasks_reviewed'] = args.tasks_reviewed
|
||||
if args.tasks_completed is not None:
|
||||
updates['tasks_completed'] = args.tasks_completed
|
||||
if updates:
|
||||
update_progress(state, **updates)
|
||||
print("Progress updated")
|
||||
|
||||
elif args.command == 'task':
|
||||
state = load_current_workflow()
|
||||
if not state:
|
||||
print("Error: No active workflow")
|
||||
sys.exit(1)
|
||||
update_task_status(state, args.task_id, args.status)
|
||||
print(f"Task {args.task_id} → {args.status}")
|
||||
|
||||
elif args.command == 'archive':
|
||||
state = load_current_workflow()
|
||||
if not state:
|
||||
print("Error: No active workflow")
|
||||
sys.exit(1)
|
||||
archive_workflow(state, args.suffix)
|
||||
print(f"Workflow archived to: {get_history_dir()}/{state['id']}{args.suffix}.yml")
|
||||
|
||||
elif args.command == 'exists':
|
||||
state = load_current_workflow()
|
||||
if state:
|
||||
print("true")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("false")
|
||||
sys.exit(1)
|
||||
|
||||
elif args.command == 'checkpoint':
|
||||
state = load_current_workflow()
|
||||
if not state:
|
||||
print("Error: No active workflow")
|
||||
sys.exit(1)
|
||||
|
||||
if args.action == 'save':
|
||||
if not args.description:
|
||||
print("Error: --description required for save")
|
||||
sys.exit(1)
|
||||
data = None
|
||||
if args.data:
|
||||
try:
|
||||
data = json.loads(args.data)
|
||||
except json.JSONDecodeError:
|
||||
print("Error: --data must be valid JSON")
|
||||
sys.exit(1)
|
||||
checkpoint = save_checkpoint(state, args.description, data)
|
||||
print(f"Checkpoint saved: {checkpoint['id']}")
|
||||
print(f"Description: {args.description}")
|
||||
|
||||
elif args.action == 'list':
|
||||
checkpoints = list_checkpoints(state)
|
||||
if not checkpoints:
|
||||
print("No checkpoints available")
|
||||
else:
|
||||
print("\n" + "=" * 60)
|
||||
print("CHECKPOINTS".center(60))
|
||||
print("=" * 60)
|
||||
for cp in checkpoints:
|
||||
print(f"\n ID: {cp['id']}")
|
||||
print(f" Time: {cp['timestamp'][:19]}")
|
||||
print(f" Phase: {cp['phase']}")
|
||||
print(f" Description: {cp['description']}")
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
elif args.action == 'restore':
|
||||
if restore_from_checkpoint(state, args.id):
|
||||
print("Workflow state restored successfully")
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
elif args.action == 'clear':
|
||||
clear_checkpoints(state)
|
||||
print("All checkpoints cleared")
|
||||
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
# Example Task - Created by Architect Agent
|
||||
# Copy this template when creating new tasks
|
||||
|
||||
# ============================================
|
||||
# FRONTEND TASK EXAMPLE
|
||||
# ============================================
|
||||
id: task_create_button
|
||||
type: create
|
||||
title: Create Button component
|
||||
agent: frontend
|
||||
status: pending
|
||||
priority: high
|
||||
|
||||
entity_ids:
|
||||
- comp_button
|
||||
|
||||
file_paths:
|
||||
- app/components/Button.tsx
|
||||
|
||||
dependencies: [] # No dependencies, can start immediately
|
||||
|
||||
description: |
|
||||
Create a reusable Button component with variant support.
|
||||
Must match the manifest specification for comp_button.
|
||||
|
||||
acceptance_criteria:
|
||||
- Exports Button component as named export
|
||||
- Implements ButtonProps interface matching manifest
|
||||
- Supports variant prop (primary, secondary, danger)
|
||||
- Supports size prop (sm, md, lg)
|
||||
- Supports disabled state
|
||||
- Uses Tailwind CSS for styling
|
||||
- Follows existing component patterns
|
||||
|
||||
---
|
||||
# ============================================
|
||||
# BACKEND TASK EXAMPLE
|
||||
# ============================================
|
||||
id: task_create_api_tasks
|
||||
type: create
|
||||
title: Create Tasks API endpoints
|
||||
agent: backend
|
||||
status: pending
|
||||
priority: high
|
||||
|
||||
entity_ids:
|
||||
- api_list_tasks
|
||||
- api_create_task
|
||||
|
||||
file_paths:
|
||||
- app/api/tasks/route.ts
|
||||
- app/lib/db.ts
|
||||
|
||||
dependencies:
|
||||
- task_create_db_tasks # Needs database first
|
||||
|
||||
description: |
|
||||
Implement GET and POST handlers for /api/tasks endpoint.
|
||||
GET: List all tasks with optional filtering
|
||||
POST: Create a new task
|
||||
|
||||
acceptance_criteria:
|
||||
- Exports GET function for listing tasks
|
||||
- Exports POST function for creating tasks
|
||||
- GET supports ?status and ?search query params
|
||||
- POST validates required title field
|
||||
- Returns proper HTTP status codes
|
||||
- Matches manifest request/response schemas
|
||||
|
||||
---
|
||||
# ============================================
|
||||
# REVIEW TASK EXAMPLE
|
||||
# ============================================
|
||||
id: task_review_button
|
||||
type: review
|
||||
title: Review Button component implementation
|
||||
agent: reviewer
|
||||
status: pending
|
||||
priority: medium
|
||||
|
||||
entity_ids:
|
||||
- comp_button
|
||||
|
||||
file_paths:
|
||||
- app/components/Button.tsx
|
||||
|
||||
dependencies:
|
||||
- task_create_button # Must complete implementation first
|
||||
|
||||
description: |
|
||||
Review the Button component implementation for quality,
|
||||
correctness, and adherence to manifest specifications.
|
||||
|
||||
acceptance_criteria:
|
||||
- Code matches manifest spec
|
||||
- Props interface is correct
|
||||
- Follows project patterns
|
||||
- Lint passes
|
||||
- Type-check passes
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
#!/bin/bash
|
||||
# Auto-generated by Eureka Factory
|
||||
# This script starts Claude Code with the workflow command
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
claude '/workflow:spawn --auto a platform where artists can publish their original songs'
|
||||
Loading…
Reference in New Issue