diff --git a/.claude/commands/agent-work-orders/commit.md b/.claude/commands/agent-work-orders/commit.md new file mode 100644 index 00000000..14f8d834 --- /dev/null +++ b/.claude/commands/agent-work-orders/commit.md @@ -0,0 +1,55 @@ +# Create Git Commit + +Create an atomic git commit with a properly formatted commit message following best practices for the uncommited changes or these specific files if specified. + +Specific files (skip if not specified): + +- File 1: $1 +- File 2: $2 +- File 3: $3 +- File 4: $4 +- File 5: $5 + +## Instructions + +**Commit Message Format:** + +- Use conventional commits: `: ` +- Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore` +- Present tense (e.g., "add", "fix", "update", not "added", "fixed", "updated") +- 50 characters or less for the subject line +- Lowercase subject line +- No period at the end +- Be specific and descriptive + +**Examples:** + +- `feat: add web search tool with structured logging` +- `fix: resolve type errors in middleware` +- `test: add unit tests for config module` +- `docs: update CLAUDE.md with testing guidelines` +- `refactor: simplify logging configuration` +- `chore: update dependencies` + +**Atomic Commits:** + +- One logical change per commit +- If you've made multiple unrelated changes, consider splitting into separate commits +- Commit should be self-contained and not break the build + +**IMPORTANT** + +- NEVER mention claude code, anthropic, co authored by or anything similar in the commit messages + +## Run + +1. Review changes: `git diff HEAD` +2. Check status: `git status` +3. Stage changes: `git add -A` +4. Create commit: `git commit -m ": "` + +## Report + +- Output the commit message used +- Confirm commit was successful with commit hash +- List files that were committed diff --git a/.claude/commands/agent-work-orders/execute.md b/.claude/commands/agent-work-orders/execute.md new file mode 100644 index 00000000..427973e6 --- /dev/null +++ b/.claude/commands/agent-work-orders/execute.md @@ -0,0 +1,27 @@ +# Execute PRP Plan + +Implement a feature plan from the PRPs directory by following its Step by Step Tasks section. + +## Variables + +Plan file: $ARGUMENTS + +## Instructions + +- Read the entire plan file carefully +- Execute **every step** in the "Step by Step Tasks" section in order, top to bottom +- Follow the "Testing Strategy" to create proper unit and integration tests +- Complete all "Validation Commands" at the end +- Ensure all linters pass and all tests pass before finishing +- Follow CLAUDE.md guidelines for type safety, logging, and docstrings + +## When done + +- Move the PRP file to the completed directory in PRPs/features/completed + +## Report + +- Summarize completed work in a concise bullet point list +- Show files and lines changed: `git diff --stat` +- Confirm all validation commands passed +- Note any deviations from the plan (if any) diff --git a/.claude/commands/agent-work-orders/noqa.md b/.claude/commands/agent-work-orders/noqa.md new file mode 100644 index 00000000..7bf8a67c --- /dev/null +++ b/.claude/commands/agent-work-orders/noqa.md @@ -0,0 +1,176 @@ +# NOQA Analysis and Resolution + +Find all noqa/type:ignore comments in the codebase, investigate why they exist, and provide recommendations for resolution or justification. + +## Instructions + +**Step 1: Find all NOQA comments** + +- Use Grep tool to find all noqa comments: pattern `noqa|type:\s*ignore` +- Use output_mode "content" with line numbers (-n flag) +- Search across all Python files (type: "py") +- Document total count of noqa comments found + +**Step 2: For EACH noqa comment (repeat this process):** + +- Read the file containing the noqa comment with sufficient context (at least 10 lines before and after) +- Identify the specific linting rule or type error being suppressed +- Understand the code's purpose and why the suppression was added +- Investigate if the suppression is still necessary or can be resolved + +**Step 3: Investigation checklist for each noqa:** + +- What specific error/warning is being suppressed? (e.g., `type: ignore[arg-type]`, `noqa: F401`) +- Why was the suppression necessary? (legacy code, false positive, legitimate limitation, technical debt) +- Can the underlying issue be fixed? (refactor code, update types, improve imports) +- What would it take to remove the suppression? (effort estimate, breaking changes, architectural changes) +- Is the suppression justified long-term? (external library limitation, Python limitation, intentional design) + +**Step 4: Research solutions:** + +- Check if newer versions of tools (mypy, ruff) handle the case better +- Look for alternative code patterns that avoid the suppression +- Consider if type stubs or Protocol definitions could help +- Evaluate if refactoring would be worthwhile + +## Report Format + +Create a markdown report file (create the reports directory if not created yet): `PRPs/reports/noqa-analysis-{YYYY-MM-DD}.md` + +Use this structure for the report: + +````markdown +# NOQA Analysis Report + +**Generated:** {date} +**Total NOQA comments found:** {count} + +--- + +## Summary + +- Total suppressions: {count} +- Can be removed: {count} +- Should remain: {count} +- Requires investigation: {count} + +--- + +## Detailed Analysis + +### 1. {File path}:{line number} + +**Location:** `{file_path}:{line_number}` + +**Suppression:** `{noqa comment or type: ignore}` + +**Code context:** + +```python +{relevant code snippet} +``` +```` + +**Why it exists:** +{explanation of why the suppression was added} + +**Options to resolve:** + +1. {Option 1: description} + - Effort: {Low/Medium/High} + - Breaking: {Yes/No} + - Impact: {description} + +2. {Option 2: description} + - Effort: {Low/Medium/High} + - Breaking: {Yes/No} + - Impact: {description} + +**Tradeoffs:** + +- {Tradeoff 1} +- {Tradeoff 2} + +**Recommendation:** {Remove | Keep | Refactor} +{Justification for recommendation} + +--- + +{Repeat for each noqa comment} + +```` + +## Example Analysis Entry + +```markdown +### 1. src/shared/config.py:45 + +**Location:** `src/shared/config.py:45` + +**Suppression:** `# type: ignore[assignment]` + +**Code context:** +```python +@property +def openai_api_key(self) -> str: + key = os.getenv("OPENAI_API_KEY") + if not key: + raise ValueError("OPENAI_API_KEY not set") + return key # type: ignore[assignment] +```` + +**Why it exists:** +MyPy cannot infer that the ValueError prevents None from being returned, so it thinks the return type could be `str | None`. + +**Options to resolve:** + +1. Use assert to help mypy narrow the type + - Effort: Low + - Breaking: No + - Impact: Cleaner code, removes suppression + +2. Add explicit cast with typing.cast() + - Effort: Low + - Breaking: No + - Impact: More verbose but type-safe + +3. Refactor to use separate validation method + - Effort: Medium + - Breaking: No + - Impact: Better separation of concerns + +**Tradeoffs:** + +- Option 1 (assert) is cleanest but asserts can be disabled with -O flag +- Option 2 (cast) is most explicit but adds import and verbosity +- Option 3 is most robust but requires more refactoring + +**Recommendation:** Remove (use Option 1) +Replace the type:ignore with an assert statement after the if check. This helps mypy understand the control flow while maintaining runtime safety. The assert will never fail in practice since the ValueError is raised first. + +**Implementation:** + +```python +@property +def openai_api_key(self) -> str: + key = os.getenv("OPENAI_API_KEY") + if not key: + raise ValueError("OPENAI_API_KEY not set") + assert key is not None # Help mypy understand control flow + return key +``` + +``` + +## Report + +After completing the analysis: + +- Output the path to the generated report file +- Summarize findings: + - Total suppressions found + - How many can be removed immediately (low effort) + - How many should remain (justified) + - How many need deeper investigation or refactoring +- Highlight any quick wins (suppressions that can be removed with minimal effort) +``` diff --git a/.claude/commands/agent-work-orders/planning.md b/.claude/commands/agent-work-orders/planning.md new file mode 100644 index 00000000..039377b0 --- /dev/null +++ b/.claude/commands/agent-work-orders/planning.md @@ -0,0 +1,176 @@ +# Feature Planning + +Create a new plan to implement the `PRP` using the exact specified markdown `PRP Format`. Follow the `Instructions` to create the plan use the `Relevant Files` to focus on the right files. + +## Variables + +FEATURE $1 $2 + +## Instructions + +- IMPORTANT: You're writing a plan to implement a net new feature based on the `Feature` that will add value to the application. +- IMPORTANT: The `Feature` describes the feature that will be implemented but remember we're not implementing a new feature, we're creating the plan that will be used to implement the feature based on the `PRP Format` below. +- Create the plan in the `PRPs/features/` directory with filename: `{descriptive-name}.md` + - Replace `{descriptive-name}` with a short, descriptive name based on the feature (e.g., "add-auth-system", "implement-search", "create-dashboard") +- Use the `PRP Format` below to create the plan. +- Deeply research the codebase to understand existing patterns, architecture, and conventions before planning the feature. +- If no patterns are established or are unclear ask the user for clarifications while providing best recommendations and options +- IMPORTANT: Replace every in the `PRP Format` with the requested value. Add as much detail as needed to implement the feature successfully. +- Use your reasoning model: THINK HARD about the feature requirements, design, and implementation approach. +- Follow existing patterns and conventions in the codebase. Don't reinvent the wheel. +- Design for extensibility and maintainability. +- Deeply do web research to understand the latest trends and technologies in the field. +- Figure out latest best practices and library documentation. +- Include links to relevant resources and documentation with anchor tags for easy navigation. +- If you need a new library, use `uv add ` and report it in the `Notes` section. +- Read `CLAUDE.md` for project principles, logging rules, testing requirements, and docstring style. +- All code MUST have type annotations (strict mypy enforcement). +- Use Google-style docstrings for all functions, classes, and modules. +- Every new file in `src/` MUST have a corresponding test file in `tests/`. +- Respect requested files in the `Relevant Files` section. + +## Relevant Files + +Focus on the following files and vertical slice structure: + +**Core Files:** + +- `CLAUDE.md` - Project instructions, logging rules, testing requirements, docstring style + app/backend core files + app/frontend core files + +## PRP Format + +```md +# Feature: + +## Feature Description + + + +## User Story + +As a +I want to +So that + +## Problem Statement + + + +## Solution Statement + + + +## Relevant Files + +Use these files to implement the feature: + + + +## Relevant research docstring + +Use these documentation files and links to help with understanding the technology to use: + +- [Documentation Link 1](https://example.com/doc1) + - [Anchor tag] + - [Short summary] +- [Documentation Link 2](https://example.com/doc2) + - [Anchor tag] + - [Short summary] + +## Implementation Plan + +### Phase 1: Foundation + + + +### Phase 2: Core Implementation + + + +### Phase 3: Integration + + + +## Step by Step Tasks + +IMPORTANT: Execute every step in order, top to bottom. + + + +/test_.py` +- Add integration test in `tests/integration/` if needed> + +## Testing Strategy + +See `CLAUDE.md` for complete testing requirements. Every file in `src/` must have a corresponding test file in `tests/`. + +### Unit Tests + + + +### Integration Tests + + + +### Edge Cases + + + +## Acceptance Criteria + + + +## Validation Commands + +Execute every command to validate the feature works correctly with zero regressions. + + + +**Required validation commands:** + +- `uv run ruff check src/` - Lint check must pass +- `uv run mypy src/` - Type check must pass +- `uv run pytest tests/ -v` - All tests must pass with zero regressions + +**Run server and test core endpoints:** + +- Start server: @.claude/start-server +- Test endpoints with curl (at minimum: health check, main functionality) +- Verify structured logs show proper correlation IDs and context +- Stop server after validation + +## Notes + + +``` + +## Feature + +Extract the feature details from the `issue_json` variable (parse the JSON and use the title and body fields). + +## Report + +- Summarize the work you've just done in a concise bullet point list. +- Include the full path to the plan file you created (e.g., `PRPs/features/add-auth-system.md`) diff --git a/.claude/commands/agent-work-orders/prime.md b/.claude/commands/agent-work-orders/prime.md new file mode 100644 index 00000000..436ba62a --- /dev/null +++ b/.claude/commands/agent-work-orders/prime.md @@ -0,0 +1,28 @@ +# Prime + +Execute the following sections to understand the codebase before starting new work, then summarize your understanding. + +## Run + +- List all tracked files: `git ls-files` +- Show project structure: `tree -I '.venv|__pycache__|*.pyc|.pytest_cache|.mypy_cache|.ruff_cache' -L 3` + +## Read + +- `CLAUDE.md` - Core project instructions, principles, logging rules, testing requirements +- `python/src/agent_work_orders` - Project overview and setup (if exists) + +- Identify core files in the agent work orders directory to understand what we are woerking on and its intent + +## Report + +Provide a concise summary of: + +1. **Project Purpose**: What this application does +2. **Architecture**: Key patterns (vertical slice, FastAPI + Pydantic AI) +3. **Core Principles**: TYPE SAFETY, KISS, YAGNI +4. **Tech Stack**: Main dependencies and tools +5. **Key Requirements**: Logging, testing, type annotations +6. **Current State**: What's implemented + +Keep the summary brief (5-10 bullet points) and focused on what you need to know to contribute effectively. diff --git a/.claude/commands/agent-work-orders/prp-review.md b/.claude/commands/agent-work-orders/prp-review.md new file mode 100644 index 00000000..c4ce29d4 --- /dev/null +++ b/.claude/commands/agent-work-orders/prp-review.md @@ -0,0 +1,89 @@ +# Code Review + +Review implemented work against a PRP specification to ensure code quality, correctness, and adherence to project standards. + +## Variables + +Plan file: $ARGUMENTS (e.g., `PRPs/features/add-web-search.md`) + +## Instructions + +**Understand the Changes:** + +- Check current branch: `git branch` +- Review changes: `git diff origin/main` (or `git diff HEAD` if not on a branch) +- Read the PRP plan file to understand requirements + +**Code Quality Review:** + +- **Type Safety**: Verify all functions have type annotations, mypy passes +- **Logging**: Check structured logging is used correctly (event names, context, exception handling) +- **Docstrings**: Ensure Google-style docstrings on all functions/classes +- **Testing**: Verify unit tests exist for all new files, integration tests if needed +- **Architecture**: Confirm vertical slice structure is followed +- **CLAUDE.md Compliance**: Check adherence to core principles (KISS, YAGNI, TYPE SAFETY) + +**Validation Ruff for BE and Biome for FE:** + +- Run linters: `uv run ruff check src/ && uv run mypy src/` +- Run tests: `uv run pytest tests/ -v` +- Start server and test endpoints with curl (if applicable) +- Verify structured logs show proper correlation IDs and context + +**Issue Severity:** + +- `blocker` - Must fix before merge (breaks build, missing tests, type errors, security issues) +- `major` - Should fix (missing logging, incomplete docstrings, poor patterns) +- `minor` - Nice to have (style improvements, optimization opportunities) + +## Report + +Return ONLY valid JSON (no markdown, no explanations) save to [report-#.json] in prps/reports directory create the directory if it doesn't exist. Output will be parsed with JSON.parse(). + +### Output Structure + +```json +{ + "success": "boolean - true if NO BLOCKER issues, false if BLOCKER issues exist", + "review_summary": "string - 2-4 sentences: what was built, does it match spec, quality assessment", + "review_issues": [ + { + "issue_number": "number - issue index", + "file_path": "string - file with the issue (if applicable)", + "issue_description": "string - what's wrong", + "issue_resolution": "string - how to fix it", + "severity": "string - blocker|major|minor" + } + ], + "validation_results": { + "linting_passed": "boolean", + "type_checking_passed": "boolean", + "tests_passed": "boolean", + "api_endpoints_tested": "boolean - true if endpoints were tested with curl" + } +} +``` + +## Example Success Review + +```json +{ + "success": true, + "review_summary": "The web search tool has been implemented with proper type annotations, structured logging, and comprehensive tests. The implementation follows the vertical slice architecture and matches all spec requirements. Code quality is high with proper error handling and documentation.", + "review_issues": [ + { + "issue_number": 1, + "file_path": "src/tools/web_search/tool.py", + "issue_description": "Missing debug log for API response", + "issue_resolution": "Add logger.debug with response metadata", + "severity": "minor" + } + ], + "validation_results": { + "linting_passed": true, + "type_checking_passed": true, + "tests_passed": true, + "api_endpoints_tested": true + } +} +``` diff --git a/.claude/commands/agent-work-orders/start-server.md b/.claude/commands/agent-work-orders/start-server.md new file mode 100644 index 00000000..58a7ce2f --- /dev/null +++ b/.claude/commands/agent-work-orders/start-server.md @@ -0,0 +1,33 @@ +# Start Servers + +Start both the FastAPI backend and React frontend development servers with hot reload. + +## Run + +### Run in the background with bash tool + +- Ensure you are in the right PWD +- Use the Bash tool to run the servers in the background so you can read the shell outputs +- IMPORTANT: run `git ls-files` first so you know where directories are located before you start + +### Backend Server (FastAPI) + +- Navigate to backend: `cd app/backend` +- Start server in background: `uv sync && uv run python run_api.py` +- Wait 2-3 seconds for startup +- Test health endpoint: `curl http://localhost:8000/health` +- Test products endpoint: `curl http://localhost:8000/api/products` + +### Frontend Server (Bun + React) + +- Navigate to frontend: `cd ../app/frontend` +- Start server in background: `bun install && bun dev` +- Wait 2-3 seconds for startup +- Frontend should be accessible at `http://localhost:3000` + +## Report + +- Confirm backend is running on `http://localhost:8000` +- Confirm frontend is running on `http://localhost:3000` +- Show the health check response from backend +- Mention: "Backend logs will show structured JSON logging for all requests" diff --git a/.env.example b/.env.example index 9647c8fa..1b68847e 100644 --- a/.env.example +++ b/.env.example @@ -27,15 +27,59 @@ SUPABASE_SERVICE_KEY= LOGFIRE_TOKEN= LOG_LEVEL=INFO +# Claude API Key (Required for Agent Work Orders) +# Get your API key from: https://console.anthropic.com/ +# Required for the agent work orders service to execute Claude CLI commands +ANTHROPIC_API_KEY= +# Generate an OAUTH token in terminal and it will use your Claude OAUTH token from your subscription. +CLAUDE_CODE_OAUTH_TOKEN= + + +# GitHub Personal Access Token (Required for Agent Work Orders PR creation) +# Get your token from: https://github.com/settings/tokens +# Required scopes: repo, workflow +# The agent work orders service uses this for gh CLI authentication to create PRs +GITHUB_PAT_TOKEN= + # Service Ports Configuration # These ports are used for external access to the services HOST=localhost ARCHON_SERVER_PORT=8181 ARCHON_MCP_PORT=8051 ARCHON_AGENTS_PORT=8052 +# Agent Work Orders Port (Optional - only needed if feature is enabled) +# Leave unset or comment out if you don't plan to use agent work orders +AGENT_WORK_ORDERS_PORT=8053 ARCHON_UI_PORT=3737 ARCHON_DOCS_PORT=3838 +# Agent Work Orders Feature (Optional) +# Enable the agent work orders microservice for automated task execution +# Default: false (feature disabled) +# Set to "true" to enable: ENABLE_AGENT_WORK_ORDERS=true +# When enabled, requires Claude API key and GitHub PAT (see above) +ENABLE_AGENT_WORK_ORDERS=true + +# Agent Work Orders Service Configuration (Optional) +# Only needed if ENABLE_AGENT_WORK_ORDERS=true +# Set these if running agent work orders service independently +# SERVICE_DISCOVERY_MODE: Controls how services find each other +# - "local": Services run on localhost with different ports +# - "docker_compose": Services use Docker container names +SERVICE_DISCOVERY_MODE=local + +# Service URLs (for agent work orders service to call other services) +# These are automatically configured based on SERVICE_DISCOVERY_MODE +# Only override if you need custom service URLs +# ARCHON_SERVER_URL=http://localhost:8181 +# ARCHON_MCP_URL=http://localhost:8051 + +# Agent Work Orders Persistence +# STATE_STORAGE_TYPE: "memory" (default, ephemeral) or "file" (persistent) +# FILE_STATE_DIRECTORY: Directory for file-based state storage +STATE_STORAGE_TYPE=file +FILE_STATE_DIRECTORY=agent-work-orders-state + # Frontend Configuration # VITE_ALLOWED_HOSTS: Comma-separated list of additional hosts allowed for Vite dev server # Example: VITE_ALLOWED_HOSTS=192.168.1.100,myhost.local,example.com diff --git a/.gitignore b/.gitignore index d1e415cb..7a5f4d0e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ __pycache__ PRPs/local PRPs/completed/ PRPs/stories/ +PRPs/examples +PRPs/features +PRPs/specs PRPs/reviews/ /logs/ .zed @@ -12,6 +15,15 @@ tmp/ temp/ UAT/ +# Temporary validation/report markdown files +/*_RESULTS.md +/*_SUMMARY.md +/*_REPORT.md +/*_SUCCESS.md +/*_COMPLETION*.md +/ACTUAL_*.md +/VALIDATION_*.md + .DS_Store # Local release notes testing diff --git a/CLAUDE.md b/CLAUDE.md index 85bbcf31..fbe5b801 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -104,12 +104,19 @@ uv run ruff check # Run linter uv run ruff check --fix # Auto-fix linting issues uv run mypy src/ # Type check +# Agent Work Orders Service (independent microservice) +make agent-work-orders # Run agent work orders service locally on 8053 +# Or manually: +uv run python -m uvicorn src.agent_work_orders.server:app --port 8053 --reload + # Docker operations docker compose up --build -d # Start all services docker compose --profile backend up -d # Backend only (for hybrid dev) -docker compose logs -f archon-server # View server logs -docker compose logs -f archon-mcp # View MCP server logs -docker compose restart archon-server # Restart after code changes +docker compose --profile work-orders up -d # Include agent work orders service +docker compose logs -f archon-server # View server logs +docker compose logs -f archon-mcp # View MCP server logs +docker compose logs -f archon-agent-work-orders # View agent work orders service logs +docker compose restart archon-server # Restart after code changes docker compose down # Stop all services docker compose down -v # Stop and remove volumes ``` @@ -120,8 +127,19 @@ docker compose down -v # Stop and remove volumes # Hybrid development (recommended) - backend in Docker, frontend local make dev # Or manually: docker compose --profile backend up -d && cd archon-ui-main && npm run dev +# Hybrid with Agent Work Orders Service - backend in Docker, agent work orders local +make dev-work-orders # Starts backend in Docker, prompts to run agent service in separate terminal +# Then in separate terminal: +make agent-work-orders # Start agent work orders service locally + # Full Docker mode make dev-docker # Or: docker compose up --build -d +docker compose --profile work-orders up -d # Include agent work orders service + +# All Local (3 terminals) - for agent work orders service development +# Terminal 1: uv run python -m uvicorn src.server.main:app --port 8181 --reload +# Terminal 2: make agent-work-orders +# Terminal 3: cd archon-ui-main && npm run dev # Run linters before committing make lint # Runs both frontend and backend linters diff --git a/Makefile b/Makefile index 5fafd66a..632153c7 100644 --- a/Makefile +++ b/Makefile @@ -5,23 +5,27 @@ SHELL := /bin/bash # Docker compose command - prefer newer 'docker compose' plugin over standalone 'docker-compose' COMPOSE ?= $(shell docker compose version >/dev/null 2>&1 && echo "docker compose" || echo "docker-compose") -.PHONY: help dev dev-docker stop test test-fe test-be lint lint-fe lint-be clean install check +.PHONY: help dev dev-docker dev-docker-full dev-work-orders dev-hybrid-work-orders stop test test-fe test-be lint lint-fe lint-be clean install check agent-work-orders help: @echo "Archon Development Commands" @echo "===========================" - @echo " make dev - Backend in Docker, frontend local (recommended)" - @echo " make dev-docker - Everything in Docker" - @echo " make stop - Stop all services" - @echo " make test - Run all tests" - @echo " make test-fe - Run frontend tests only" - @echo " make test-be - Run backend tests only" - @echo " make lint - Run all linters" - @echo " make lint-fe - Run frontend linter only" - @echo " make lint-be - Run backend linter only" - @echo " make clean - Remove containers and volumes" - @echo " make install - Install dependencies" - @echo " make check - Check environment setup" + @echo " make dev - Backend in Docker, frontend local (recommended)" + @echo " make dev-docker - Backend + frontend in Docker" + @echo " make dev-docker-full - Everything in Docker (server + mcp + ui + work orders)" + @echo " make dev-hybrid-work-orders - Server + MCP in Docker, UI + work orders local (2 terminals)" + @echo " make dev-work-orders - Backend in Docker, agent work orders local, frontend local" + @echo " make agent-work-orders - Run agent work orders service locally" + @echo " make stop - Stop all services" + @echo " make test - Run all tests" + @echo " make test-fe - Run frontend tests only" + @echo " make test-be - Run backend tests only" + @echo " make lint - Run all linters" + @echo " make lint-fe - Run frontend linter only" + @echo " make lint-be - Run backend linter only" + @echo " make clean - Remove containers and volumes" + @echo " make install - Install dependencies" + @echo " make check - Check environment setup" # Install dependencies install: @@ -54,18 +58,73 @@ dev: check VITE_ARCHON_SERVER_HOST=$${HOST:-} \ npm run dev -# Full Docker development +# Full Docker development (backend + frontend, no work orders) dev-docker: check - @echo "Starting full Docker environment..." + @echo "Starting Docker environment (backend + frontend)..." @$(COMPOSE) --profile full up -d --build - @echo "✓ All services running" + @echo "✓ Services running" @echo "Frontend: http://localhost:3737" @echo "API: http://localhost:8181" +# Full Docker with all services (server + mcp + ui + agent work orders) +dev-docker-full: check + @echo "Starting full Docker environment with agent work orders..." + @$(COMPOSE) up archon-server archon-mcp archon-frontend archon-agent-work-orders -d --build + @set -a; [ -f .env ] && . ./.env; set +a; \ + echo "✓ All services running"; \ + echo "Frontend: http://localhost:3737"; \ + echo "API: http://$${HOST:-localhost}:$${ARCHON_SERVER_PORT:-8181}"; \ + echo "MCP: http://$${HOST:-localhost}:$${ARCHON_MCP_PORT:-8051}"; \ + echo "Agent Work Orders: http://$${HOST:-localhost}:$${AGENT_WORK_ORDERS_PORT:-8053}" + +# Agent work orders service locally (standalone) +agent-work-orders: + @echo "Starting Agent Work Orders service locally..." + @set -a; [ -f .env ] && . ./.env; set +a; \ + export SERVICE_DISCOVERY_MODE=local; \ + export ARCHON_SERVER_URL=http://localhost:$${ARCHON_SERVER_PORT:-8181}; \ + export ARCHON_MCP_URL=http://localhost:$${ARCHON_MCP_PORT:-8051}; \ + export AGENT_WORK_ORDERS_PORT=$${AGENT_WORK_ORDERS_PORT:-8053}; \ + cd python && uv run python -m uvicorn src.agent_work_orders.server:app --host 0.0.0.0 --port $${AGENT_WORK_ORDERS_PORT:-8053} --reload + +# Hybrid development with agent work orders (backend in Docker, agent work orders local, frontend local) +dev-work-orders: check + @echo "Starting hybrid development with agent work orders..." + @echo "Backend: Docker | Agent Work Orders: Local | Frontend: Local" + @$(COMPOSE) up archon-server archon-mcp -d --build + @set -a; [ -f .env ] && . ./.env; set +a; \ + echo "Backend running at http://$${HOST:-localhost}:$${ARCHON_SERVER_PORT:-8181}"; \ + echo "Starting agent work orders service..."; \ + echo "Run in separate terminal: make agent-work-orders"; \ + echo "Starting frontend..."; \ + cd archon-ui-main && \ + VITE_ARCHON_SERVER_PORT=$${ARCHON_SERVER_PORT:-8181} \ + VITE_ARCHON_SERVER_HOST=$${HOST:-} \ + npm run dev + +# Hybrid development: Server + MCP in Docker, UI + Work Orders local (requires 2 terminals) +dev-hybrid-work-orders: check + @echo "Starting hybrid development: Server + MCP in Docker, UI + Work Orders local" + @echo "================================================================" + @$(COMPOSE) up archon-server archon-mcp -d --build + @set -a; [ -f .env ] && . ./.env; set +a; \ + echo ""; \ + echo "✓ Server + MCP running in Docker"; \ + echo " Server: http://$${HOST:-localhost}:$${ARCHON_SERVER_PORT:-8181}"; \ + echo " MCP: http://$${HOST:-localhost}:$${ARCHON_MCP_PORT:-8051}"; \ + echo ""; \ + echo "Next steps:"; \ + echo " 1. Terminal 1 (this one): Press Ctrl+C when done"; \ + echo " 2. Terminal 2: make agent-work-orders"; \ + echo " 3. Terminal 3: cd archon-ui-main && npm run dev"; \ + echo ""; \ + echo "Or use 'make dev-docker-full' to run everything in Docker."; \ + @read -p "Press Enter to continue or Ctrl+C to stop..." _ + # Stop all services stop: @echo "Stopping all services..." - @$(COMPOSE) --profile backend --profile frontend --profile full down + @$(COMPOSE) --profile backend --profile frontend --profile full --profile work-orders down @echo "✓ Services stopped" # Run all tests diff --git a/PRPs/ai_docs/AGENT_WORK_ORDERS_SSE_AND_ZUSTAND.md b/PRPs/ai_docs/AGENT_WORK_ORDERS_SSE_AND_ZUSTAND.md new file mode 100644 index 00000000..d880adb6 --- /dev/null +++ b/PRPs/ai_docs/AGENT_WORK_ORDERS_SSE_AND_ZUSTAND.md @@ -0,0 +1,1327 @@ +# Agent Work Orders: SSE + Zustand State Management Standards + +## Purpose + +This document defines the **complete architecture, patterns, and standards** for implementing Zustand state management with Server-Sent Events (SSE) in the Agent Work Orders feature. It serves as the authoritative reference for: + +- State management boundaries (what goes in Zustand vs TanStack Query vs local useState) +- SSE integration patterns and connection management +- Zustand slice organization and naming conventions +- Anti-patterns to avoid +- Migration strategy and implementation plan + +**This is a pilot feature** - patterns established here will be applied to other features (Knowledge Base, Projects, Settings). + +--- + +## Current State Analysis + +### Component Structure +- **Total Lines:** ~4,400 lines +- **Components:** 10 (RepositoryCard, WorkOrderTable, modals, etc.) +- **Views:** 2 (AgentWorkOrdersView, AgentWorkOrderDetailView) +- **Hooks:** 4 (useAgentWorkOrderQueries, useRepositoryQueries, useWorkOrderLogs, useLogStats) +- **Services:** 2 (agentWorkOrdersService, repositoryService) + +### Current State Management (42 useState calls) + +**AgentWorkOrdersView (8 state variables):** +```typescript +const [layoutMode, setLayoutMode] = useState(getInitialLayoutMode); +const [sidebarExpanded, setSidebarExpanded] = useState(true); +const [showAddRepoModal, setShowAddRepoModal] = useState(false); +const [showEditRepoModal, setShowEditRepoModal] = useState(false); +const [editingRepository, setEditingRepository] = useState(null); +const [showNewWorkOrderModal, setShowNewWorkOrderModal] = useState(false); +const [searchQuery, setSearchQuery] = useState(""); +const selectedRepositoryId = searchParams.get("repo") || undefined; +``` + +**Problems:** +- Manual localStorage management (layoutMode) +- Prop drilling for modal controls +- No persistence for searchQuery or sidebarExpanded +- Scattered state across multiple useState calls + +--- + +## SSE Architecture (Already Implemented!) + +### Backend SSE Streams + +**1. Log Stream (✅ Complete)** +``` +GET /api/agent-work-orders/{id}/logs/stream +``` + +**What it provides:** +- Real-time structured logs from workflow execution +- Event types: `workflow_started`, `step_started`, `step_completed`, `workflow_completed`, `workflow_failed` +- Rich metadata in each log: `step`, `step_number`, `total_steps`, `progress`, `progress_pct`, `elapsed_seconds` +- Filters: level, step, since timestamp +- Heartbeat every 15 seconds + +**Frontend Integration:** +- ✅ `useWorkOrderLogs` hook - EventSource connection with auto-reconnect +- ✅ `useLogStats` hook - Parses logs to extract progress metrics +- ✅ `RealTimeStats` component - Now uses real SSE data (was mock) +- ✅ `ExecutionLogs` component - Now displays real logs (was mock) + +**Key Insight:** SSE logs contain ALL progress information including: +- Current step and progress percentage +- Elapsed time +- Step completion status +- Git stats (from log events) +- Workflow lifecycle events + +--- + +### Current Polling (Should Be Replaced) + +**useWorkOrders() - Polls every 3s:** +```typescript +refetchInterval: (query) => { + const hasActiveWorkOrders = data?.some((wo) => wo.status === "running" || wo.status === "pending"); + return hasActiveWorkOrders ? 3000 : false; +} +``` + +**useWorkOrder(id) - Polls every 3s:** +```typescript +refetchInterval: (query) => { + if (data?.status === "running" || data?.status === "pending") { + return 3000; + } + return false; +} +``` + +**useStepHistory(id) - Polls every 3s:** +```typescript +refetchInterval: (query) => { + const lastStep = history?.steps[history.steps.length - 1]; + if (lastStep?.step === "create-pr" && lastStep?.success) { + return false; + } + return 3000; +} +``` + +**Network Impact:** +- 3 active work orders = ~140 HTTP requests/minute +- With ETags: ~50-100KB/minute bandwidth +- Up to 3 second delay for updates + +--- + +## Zustand State Management Standards + +### Core Principles + +**1. State Categorization:** +- **UI Preferences** → Zustand (persisted) +- **Modal State** → Zustand (NOT persisted) +- **Filter State** → Zustand (persisted) +- **SSE Connections** → Zustand (NOT persisted) +- **Server Data** → TanStack Query (cached) +- **Form State** → Zustand slices OR local useState (depends on complexity) +- **Ephemeral UI** → Local useState (component-specific) + +**2. Selective Subscriptions:** +```typescript +// ✅ GOOD - Only re-renders when layoutMode changes +const layoutMode = useAgentWorkOrdersStore((s) => s.layoutMode); +const setLayoutMode = useAgentWorkOrdersStore((s) => s.setLayoutMode); + +// ❌ BAD - Re-renders on ANY state change +const { layoutMode, searchQuery, selectedRepositoryId } = useAgentWorkOrdersStore(); +``` + +**3. Server State Boundary:** +```typescript +// ✅ GOOD - TanStack Query for initial load, mutations, caching +const { data: repositories } = useRepositories(); + +// ✅ GOOD - Zustand for real-time SSE updates +const liveWorkOrder = useAgentWorkOrdersStore((s) => s.liveWorkOrders[id]); + +// ✅ GOOD - Combine them +const workOrder = liveWorkOrder || cachedWorkOrder; // SSE overrides cache + +// ❌ BAD - Duplicating server state in Zustand +const repositories = useAgentWorkOrdersStore((s) => s.repositories); // DON'T DO THIS +``` + +**4. Slice Organization:** +- One slice per concern (modals, UI prefs, filters, SSE) +- Each slice is independently testable +- Slices can reference each other via get() +- Use TypeScript for all slice types + +--- + +## Zustand Store Structure + +### File Organization +``` +src/features/agent-work-orders/state/ +├── agentWorkOrdersStore.ts # Main store combining slices +├── slices/ +│ ├── uiPreferencesSlice.ts # Layout, sidebar state +│ ├── modalsSlice.ts # Modal visibility & context +│ ├── filtersSlice.ts # Search, selected repo +│ └── sseSlice.ts # SSE connections & live data +└── __tests__/ + └── agentWorkOrdersStore.test.ts # Store tests +``` + +--- + +### Main Store (agentWorkOrdersStore.ts) + +```typescript +import { create } from 'zustand'; +import { persist, devtools, subscribeWithSelector } from 'zustand/middleware'; +import { createUIPreferencesSlice, type UIPreferencesSlice } from './slices/uiPreferencesSlice'; +import { createModalsSlice, type ModalsSlice } from './slices/modalsSlice'; +import { createFiltersSlice, type FiltersSlice } from './slices/filtersSlice'; +import { createSSESlice, type SSESlice } from './slices/sseSlice'; + +/** + * Combined Agent Work Orders store type + * Combines all slices into a single store interface + */ +export type AgentWorkOrdersStore = + & UIPreferencesSlice + & ModalsSlice + & FiltersSlice + & SSESlice; + +/** + * Agent Work Orders global state store + * + * Manages: + * - UI preferences (layout mode, sidebar state) - PERSISTED + * - Modal state (which modal is open, editing context) - NOT persisted + * - Filter state (search query, selected repository) - PERSISTED + * - SSE connections (live updates, connection management) - NOT persisted + * + * Does NOT manage: + * - Server data (TanStack Query handles this) + * - Ephemeral UI state (local useState for row expansion, etc.) + */ +export const useAgentWorkOrdersStore = create()( + devtools( + subscribeWithSelector( + persist( + (...a) => ({ + ...createUIPreferencesSlice(...a), + ...createModalsSlice(...a), + ...createFiltersSlice(...a), + ...createSSESlice(...a), + }), + { + name: 'agent-work-orders-ui', + version: 1, + partialize: (state) => ({ + // Only persist UI preferences and filters + layoutMode: state.layoutMode, + sidebarExpanded: state.sidebarExpanded, + searchQuery: state.searchQuery, + // Do NOT persist: + // - Modal state (ephemeral) + // - SSE connections (must be re-established) + // - Live data (should be fresh on reload) + }), + } + ) + ), + { name: 'AgentWorkOrders' } + ) +); +``` + +--- + +### UI Preferences Slice + +```typescript +// src/features/agent-work-orders/state/slices/uiPreferencesSlice.ts + +import { StateCreator } from 'zustand'; + +export type LayoutMode = 'horizontal' | 'sidebar'; + +export type UIPreferencesSlice = { + // State + layoutMode: LayoutMode; + sidebarExpanded: boolean; + + // Actions + setLayoutMode: (mode: LayoutMode) => void; + setSidebarExpanded: (expanded: boolean) => void; + toggleSidebar: () => void; + resetUIPreferences: () => void; +}; + +/** + * UI Preferences Slice + * + * Manages user interface preferences that should persist across sessions. + * Includes layout mode (horizontal/sidebar) and sidebar expansion state. + * + * Persisted: YES (via persist middleware in main store) + */ +export const createUIPreferencesSlice: StateCreator< + UIPreferencesSlice, + [], + [], + UIPreferencesSlice +> = (set) => ({ + // Initial state + layoutMode: 'sidebar', + sidebarExpanded: true, + + // Actions + setLayoutMode: (mode) => set({ layoutMode: mode }), + + setSidebarExpanded: (expanded) => set({ sidebarExpanded: expanded }), + + toggleSidebar: () => set((state) => ({ sidebarExpanded: !state.sidebarExpanded })), + + resetUIPreferences: () => + set({ + layoutMode: 'sidebar', + sidebarExpanded: true, + }), +}); +``` + +**Replaces:** +- Manual localStorage get/set (~20 lines eliminated) +- getInitialLayoutMode, saveLayoutMode functions +- useState for layoutMode and sidebarExpanded + +--- + +### Modals Slice (With Optional Form State) + +```typescript +// src/features/agent-work-orders/state/slices/modalsSlice.ts + +import { StateCreator } from 'zustand'; +import type { ConfiguredRepository } from '../../types/repository'; +import type { WorkflowStep } from '../../types'; + +export type ModalsSlice = { + // Modal visibility + showAddRepoModal: boolean; + showEditRepoModal: boolean; + showCreateWorkOrderModal: boolean; + + // Modal context (which item is being edited) + editingRepository: ConfiguredRepository | null; + preselectedRepositoryId: string | undefined; + + // Actions + openAddRepoModal: () => void; + closeAddRepoModal: () => void; + openEditRepoModal: (repository: ConfiguredRepository) => void; + closeEditRepoModal: () => void; + openCreateWorkOrderModal: (repositoryId?: string) => void; + closeCreateWorkOrderModal: () => void; + closeAllModals: () => void; +}; + +/** + * Modals Slice + * + * Manages modal visibility and context (which repository is being edited, etc.). + * Enables opening modals from anywhere without prop drilling. + * + * Persisted: NO (modals should not persist across page reloads) + * + * Note: Form state (repositoryUrl, selectedSteps, etc.) can be added to this slice + * if centralized validation/submission logic is desired. For simple forms that + * reset on close, local useState in the modal component is cleaner. + */ +export const createModalsSlice: StateCreator< + ModalsSlice, + [], + [], + ModalsSlice +> = (set) => ({ + // Initial state + showAddRepoModal: false, + showEditRepoModal: false, + showCreateWorkOrderModal: false, + editingRepository: null, + preselectedRepositoryId: undefined, + + // Actions + openAddRepoModal: () => set({ showAddRepoModal: true }), + + closeAddRepoModal: () => set({ showAddRepoModal: false }), + + openEditRepoModal: (repository) => + set({ + showEditRepoModal: true, + editingRepository: repository, + }), + + closeEditRepoModal: () => + set({ + showEditRepoModal: false, + editingRepository: null, + }), + + openCreateWorkOrderModal: (repositoryId) => + set({ + showCreateWorkOrderModal: true, + preselectedRepositoryId: repositoryId, + }), + + closeCreateWorkOrderModal: () => + set({ + showCreateWorkOrderModal: false, + preselectedRepositoryId: undefined, + }), + + closeAllModals: () => + set({ + showAddRepoModal: false, + showEditRepoModal: false, + showCreateWorkOrderModal: false, + editingRepository: null, + preselectedRepositoryId: undefined, + }), +}); +``` + +**Replaces:** +- Multiple useState calls for modal visibility (~5 states) +- handleEditRepository, handleCreateWorkOrder helper functions +- Prop drilling for modal open/close callbacks + +--- + +### Filters Slice + +```typescript +// src/features/agent-work-orders/state/slices/filtersSlice.ts + +import { StateCreator } from 'zustand'; + +export type FiltersSlice = { + // State + searchQuery: string; + selectedRepositoryId: string | undefined; + + // Actions + setSearchQuery: (query: string) => void; + selectRepository: (id: string | undefined, syncUrl?: (id: string | undefined) => void) => void; + clearFilters: () => void; +}; + +/** + * Filters Slice + * + * Manages filter and selection state for repositories and work orders. + * Includes search query and selected repository ID. + * + * Persisted: YES (search/selection survives reload) + * + * URL Sync: selectedRepositoryId should also update URL query params. + * Use the syncUrl callback to keep URL in sync. + */ +export const createFiltersSlice: StateCreator< + FiltersSlice, + [], + [], + FiltersSlice +> = (set) => ({ + // Initial state + searchQuery: '', + selectedRepositoryId: undefined, + + // Actions + setSearchQuery: (query) => set({ searchQuery: query }), + + selectRepository: (id, syncUrl) => { + set({ selectedRepositoryId: id }); + // Callback to sync with URL search params + syncUrl?.(id); + }, + + clearFilters: () => + set({ + searchQuery: '', + selectedRepositoryId: undefined, + }), +}); +``` + +**Replaces:** +- useState for searchQuery +- Manual selectRepository function +- Enables global filtering in future + +--- + +### SSE Slice (Replaces Polling!) + +```typescript +// src/features/agent-work-orders/state/slices/sseSlice.ts + +import { StateCreator } from 'zustand'; +import type { AgentWorkOrder, StepExecutionResult, LogEntry } from '../../types'; + +export type SSESlice = { + // Active EventSource connections (keyed by work_order_id) + logConnections: Map; + + // Connection states + connectionStates: Record; + + // Live data from SSE (keyed by work_order_id) + // This OVERLAYS on top of TanStack Query cached data + liveLogs: Record; + liveProgress: Record; + + // Actions + connectToLogs: (workOrderId: string) => void; + disconnectFromLogs: (workOrderId: string) => void; + handleLogEvent: (workOrderId: string, log: LogEntry) => void; + clearLogs: (workOrderId: string) => void; + disconnectAll: () => void; +}; + +/** + * SSE Slice + * + * Manages Server-Sent Event connections and real-time data from log streams. + * Handles connection lifecycle, auto-reconnect, and live data aggregation. + * + * Persisted: NO (connections must be re-established on page load) + * + * Pattern: + * 1. Component calls connectToLogs(workOrderId) on mount + * 2. Zustand creates EventSource if not exists + * 3. Multiple components can subscribe to same connection + * 4. handleLogEvent parses logs and updates liveProgress + * 5. Component calls disconnectFromLogs on unmount + * 6. Zustand closes EventSource when no more subscribers + */ +export const createSSESlice: StateCreator = (set, get) => ({ + // Initial state + logConnections: new Map(), + connectionStates: {}, + liveLogs: {}, + liveProgress: {}, + + // Actions + connectToLogs: (workOrderId) => { + const { logConnections, connectionStates } = get(); + + // Don't create duplicate connections + if (logConnections.has(workOrderId)) { + return; + } + + // Set connecting state + set((state) => ({ + connectionStates: { + ...state.connectionStates, + [workOrderId]: 'connecting', + }, + })); + + // Create EventSource for log stream + const url = `/api/agent-work-orders/${workOrderId}/logs/stream`; + const eventSource = new EventSource(url); + + eventSource.onopen = () => { + set((state) => ({ + connectionStates: { + ...state.connectionStates, + [workOrderId]: 'connected', + }, + })); + }; + + eventSource.onmessage = (event) => { + try { + const logEntry: LogEntry = JSON.parse(event.data); + get().handleLogEvent(workOrderId, logEntry); + } catch (err) { + console.error('Failed to parse log entry:', err); + } + }; + + eventSource.onerror = () => { + set((state) => ({ + connectionStates: { + ...state.connectionStates, + [workOrderId]: 'error', + }, + })); + + // Auto-reconnect after 5 seconds + setTimeout(() => { + eventSource.close(); + logConnections.delete(workOrderId); + get().connectToLogs(workOrderId); // Retry + }, 5000); + }; + + // Store connection + logConnections.set(workOrderId, eventSource); + set({ logConnections: new Map(logConnections) }); + }, + + disconnectFromLogs: (workOrderId) => { + const { logConnections } = get(); + const connection = logConnections.get(workOrderId); + + if (connection) { + connection.close(); + logConnections.delete(workOrderId); + + set({ + logConnections: new Map(logConnections), + connectionStates: { + ...get().connectionStates, + [workOrderId]: 'disconnected', + }, + }); + } + }, + + handleLogEvent: (workOrderId, log) => { + // Add to logs array + set((state) => ({ + liveLogs: { + ...state.liveLogs, + [workOrderId]: [...(state.liveLogs[workOrderId] || []), log].slice(-500), // Keep last 500 + }, + })); + + // Parse log to update progress + const progressUpdate: any = {}; + + if (log.event === 'step_started') { + progressUpdate.currentStep = log.step; + progressUpdate.stepNumber = log.step_number; + progressUpdate.totalSteps = log.total_steps; + } + + if (log.progress_pct !== undefined) { + progressUpdate.progressPct = log.progress_pct; + } + + if (log.elapsed_seconds !== undefined) { + progressUpdate.elapsedSeconds = log.elapsed_seconds; + } + + if (log.event === 'workflow_completed') { + progressUpdate.status = 'completed'; + } + + if (log.event === 'workflow_failed' || log.level === 'error') { + progressUpdate.status = 'failed'; + } + + if (Object.keys(progressUpdate).length > 0) { + set((state) => ({ + liveProgress: { + ...state.liveProgress, + [workOrderId]: { + ...state.liveProgress[workOrderId], + ...progressUpdate, + }, + }, + })); + } + }, + + clearLogs: (workOrderId) => { + set((state) => ({ + liveLogs: { + ...state.liveLogs, + [workOrderId]: [], + }, + })); + }, + + disconnectAll: () => { + const { logConnections } = get(); + logConnections.forEach((conn) => conn.close()); + + set({ + logConnections: new Map(), + connectionStates: {}, + liveLogs: {}, + liveProgress: {}, + }); + }, +}); +``` + +--- + +## Component Integration Patterns + +### Pattern 1: RealTimeStats (SSE + Zustand) + +**Current (just fixed):** +```typescript +export function RealTimeStats({ workOrderId }: RealTimeStatsProps) { + const { logs } = useWorkOrderLogs({ workOrderId }); // Direct SSE hook + const stats = useLogStats(logs); // Parse logs + + // Display stats.currentStep, stats.progressPct, etc. +} +``` + +**With Zustand SSE Slice:** +```typescript +export function RealTimeStats({ workOrderId }: RealTimeStatsProps) { + // Connect to SSE (Zustand manages connection) + const connectToLogs = useAgentWorkOrdersStore((s) => s.connectToLogs); + const disconnectFromLogs = useAgentWorkOrdersStore((s) => s.disconnectFromLogs); + + useEffect(() => { + connectToLogs(workOrderId); + return () => disconnectFromLogs(workOrderId); + }, [workOrderId]); + + // Subscribe to parsed progress (Zustand parses logs automatically) + const progress = useAgentWorkOrdersStore((s) => s.liveProgress[workOrderId]); + + // Display progress.currentStep, progress.progressPct, etc. + // No need for useLogStats - Zustand already parsed it! +} +``` + +**Benefits:** +- Zustand handles connection lifecycle +- Multiple components can display progress without multiple connections +- Automatic cleanup when all subscribers unmount + +--- + +### Pattern 2: WorkOrderRow (Hybrid TanStack + Zustand) + +**Current:** +```typescript +const { data: workOrder } = useWorkOrder(id); // Polls every 3s +``` + +**With Zustand:** +```typescript +// Initial load from TanStack Query (cached, no polling) +const { data: cachedWorkOrder } = useWorkOrder(id, { + refetchInterval: false, // NO MORE POLLING! +}); + +// Live updates from SSE (via Zustand) +const liveProgress = useAgentWorkOrdersStore((s) => s.liveProgress[id]); + +// Merge: SSE overrides cached data +const workOrder = { + ...cachedWorkOrder, + ...liveProgress, // status, git_commit_count, etc. from SSE +}; +``` + +**Benefits:** +- No polling (0 HTTP requests while connected) +- Instant updates from SSE +- TanStack Query still handles initial load, mutations, caching + +--- + +### Pattern 3: Modal Management (No Prop Drilling) + +**Current:** +```typescript +// AgentWorkOrdersView +const [showEditRepoModal, setShowEditRepoModal] = useState(false); +const [editingRepository, setEditingRepository] = useState(null); + +const handleEditRepository = (repository: ConfiguredRepository) => { + setEditingRepository(repository); + setShowEditRepoModal(true); +}; + +// Pass down to child + handleEditRepository(repository)} /> +``` + +**With Zustand:** +```typescript +// RepositoryCard (no props needed) +const openEditRepoModal = useAgentWorkOrdersStore((s) => s.openEditRepoModal); + + +// AgentWorkOrdersView (just renders modal) +const showEditRepoModal = useAgentWorkOrdersStore((s) => s.showEditRepoModal); +const closeEditRepoModal = useAgentWorkOrdersStore((s) => s.closeEditRepoModal); +const editingRepository = useAgentWorkOrdersStore((s) => s.editingRepository); + + +``` + +**Benefits:** +- Can open modal from anywhere (breadcrumb, keyboard shortcut, etc.) +- No callback props +- Cleaner component tree + +--- + +## Anti-Patterns (DO NOT DO) + +### ❌ Anti-Pattern 1: Subscribing to Full Store +```typescript +// BAD - Component re-renders on ANY state change +const store = useAgentWorkOrdersStore(); +const { layoutMode, searchQuery, selectedRepositoryId } = store; +``` + +**Why bad:** +- Component re-renders even if only unrelated state changes +- Defeats the purpose of Zustand's selective subscriptions + +**Fix:** +```typescript +// GOOD - Only re-renders when layoutMode changes +const layoutMode = useAgentWorkOrdersStore((s) => s.layoutMode); +``` + +--- + +### ❌ Anti-Pattern 2: Duplicating Server State +```typescript +// BAD - Storing server data in Zustand +type BadSlice = { + repositories: ConfiguredRepository[]; + workOrders: AgentWorkOrder[]; + isLoadingRepos: boolean; + fetchRepositories: () => Promise; +}; +``` + +**Why bad:** +- Reimplements TanStack Query (caching, invalidation, optimistic updates) +- Loses Query features (background refetch, deduplication, etc.) +- Increases complexity + +**Fix:** +```typescript +// GOOD - TanStack Query for server data +const { data: repositories } = useRepositories(); + +// GOOD - Zustand ONLY for SSE overlays +const liveUpdates = useAgentWorkOrdersStore((s) => s.liveWorkOrders); +``` + +--- + +### ❌ Anti-Pattern 3: Putting Everything in Global State +```typescript +// BAD - Form state in Zustand when it shouldn't be +type BadSlice = { + addRepoForm: { + repositoryUrl: string; + error: string; + isSubmitting: boolean; + }; + expandedWorkOrderRows: Set; // Per-row state in global store! +}; +``` + +**Why bad:** +- Clutters global state with component-local concerns +- Forms that reset on close don't need global state +- Row expansion is per-instance, not global + +**Fix:** +```typescript +// GOOD - Local useState for simple forms +export function AddRepositoryModal() { + const [repositoryUrl, setRepositoryUrl] = useState(""); + const [error, setError] = useState(""); + // Resets on modal close - perfect for local state +} + +// GOOD - Local useState for per-component UI +export function WorkOrderRow() { + const [isExpanded, setIsExpanded] = useState(false); + // Each row has its own expansion state +} +``` + +--- + +### ❌ Anti-Pattern 4: Using getState() in Render Logic +```typescript +// BAD - Doesn't subscribe to changes +function MyComponent() { + const layoutMode = useAgentWorkOrdersStore.getState().layoutMode; + // Component won't re-render when layoutMode changes! +} +``` + +**Why bad:** +- getState() doesn't create a subscription +- Component won't re-render on state changes +- Silent bugs + +**Fix:** +```typescript +// GOOD - Proper subscription +const layoutMode = useAgentWorkOrdersStore((s) => s.layoutMode); +``` + +--- + +### ❌ Anti-Pattern 5: Not Cleaning Up SSE Connections +```typescript +// BAD - Connection leaks +useEffect(() => { + connectToLogs(workOrderId); + // Missing cleanup! +}, [workOrderId]); +``` + +**Why bad:** +- EventSource connections stay open forever +- Memory leaks +- Browser connection limit (6 per domain) + +**Fix:** +```typescript +// GOOD - Cleanup on unmount +useEffect(() => { + connectToLogs(workOrderId); + return () => disconnectFromLogs(workOrderId); +}, [workOrderId]); +``` + +--- + +## Implementation Checklist + +### Phase 1: Zustand Foundation (Frontend Only) +- [ ] Create `agentWorkOrdersStore.ts` with slice pattern +- [ ] Create `uiPreferencesSlice.ts` (layoutMode, sidebarExpanded) +- [ ] Create `modalsSlice.ts` (modal visibility, editing context) +- [ ] Create `filtersSlice.ts` (searchQuery, selectedRepositoryId) +- [ ] Add persist middleware (only UI prefs and filters) +- [ ] Add devtools middleware +- [ ] Write store tests + +**Expected Changes:** +- +350 lines (store + slices) +- -50 lines (remove localStorage boilerplate, helper functions) +- Net: +300 lines + +--- + +### Phase 2: Migrate AgentWorkOrdersView (Frontend Only) +- [ ] Replace useState with Zustand selectors +- [ ] Remove localStorage helper functions (getInitialLayoutMode, saveLayoutMode) +- [ ] Remove modal helper functions (handleEditRepository, etc.) +- [ ] Update modal open/close to use Zustand actions +- [ ] Sync selectedRepositoryId with URL params +- [ ] Test thoroughly (layouts, modals, navigation) + +**Expected Changes:** +- AgentWorkOrdersView: -40 lines (400 → 360) +- Eliminate prop drilling for modal callbacks + +--- + +### Phase 3: SSE Integration (Frontend Only) +- [ ] Already done! RealTimeStats now uses real SSE data +- [ ] Already done! ExecutionLogs now displays real logs +- [ ] Verify SSE connection works in browser +- [ ] Check Network tab for `/logs/stream` connection +- [ ] Verify logs appear in real-time + +**Expected Changes:** +- None needed - just fixed mock data usage + +--- + +### Phase 4: Remove Polling (Frontend Only) +- [ ] Create `sseSlice.ts` for connection management +- [ ] Add `connectToLogs`, `disconnectFromLogs` actions +- [ ] Add `handleLogEvent` to parse logs and update liveProgress +- [ ] Update RealTimeStats to use Zustand SSE slice +- [ ] Remove `refetchInterval` from `useWorkOrder(id)` +- [ ] Remove `refetchInterval` from `useStepHistory(id)` +- [ ] Remove `refetchInterval` from `useWorkOrders()` (optional - list updates are less critical) +- [ ] Test that status/progress updates appear instantly + +**Expected Changes:** +- +150 lines (SSE slice) +- -40 lines (remove polling logic) +- Net: +110 lines + +--- + +### Phase 5: Testing & Documentation +- [ ] Unit tests for all slices +- [ ] Integration test: Create work order → Watch SSE updates → Verify UI updates +- [ ] Test SSE reconnection on connection loss +- [ ] Test multiple components subscribing to same work order +- [ ] Document patterns in this file +- [ ] Update ZUSTAND_STATE_MANAGEMENT.md with agent work orders examples + +--- + +## Testing Standards + +### Store Testing +```typescript +// agentWorkOrdersStore.test.ts +import { useAgentWorkOrdersStore } from './agentWorkOrdersStore'; + +describe('AgentWorkOrdersStore', () => { + beforeEach(() => { + // Reset store to initial state + useAgentWorkOrdersStore.setState({ + layoutMode: 'sidebar', + sidebarExpanded: true, + searchQuery: '', + selectedRepositoryId: undefined, + showAddRepoModal: false, + // ... reset all state + }); + }); + + it('should toggle layout mode and persist', () => { + const { setLayoutMode } = useAgentWorkOrdersStore.getState(); + setLayoutMode('horizontal'); + + expect(useAgentWorkOrdersStore.getState().layoutMode).toBe('horizontal'); + + // Check localStorage persistence + const persisted = JSON.parse(localStorage.getItem('agent-work-orders-ui') || '{}'); + expect(persisted.state.layoutMode).toBe('horizontal'); + }); + + it('should manage modal state without persistence', () => { + const { openEditRepoModal, closeEditRepoModal } = useAgentWorkOrdersStore.getState(); + const mockRepo = { id: '1', repository_url: 'https://github.com/test/repo' } as ConfiguredRepository; + + openEditRepoModal(mockRepo); + expect(useAgentWorkOrdersStore.getState().showEditRepoModal).toBe(true); + expect(useAgentWorkOrdersStore.getState().editingRepository).toBe(mockRepo); + + closeEditRepoModal(); + expect(useAgentWorkOrdersStore.getState().showEditRepoModal).toBe(false); + expect(useAgentWorkOrdersStore.getState().editingRepository).toBe(null); + + // Verify modals NOT persisted + const persisted = JSON.parse(localStorage.getItem('agent-work-orders-ui') || '{}'); + expect(persisted.state.showEditRepoModal).toBeUndefined(); + }); + + it('should handle SSE log events and parse progress', () => { + const { handleLogEvent } = useAgentWorkOrdersStore.getState(); + const workOrderId = 'wo-123'; + + const stepStartedLog: LogEntry = { + work_order_id: workOrderId, + level: 'info', + event: 'step_started', + timestamp: new Date().toISOString(), + step: 'planning', + step_number: 2, + total_steps: 5, + progress_pct: 40, + }; + + handleLogEvent(workOrderId, stepStartedLog); + + const progress = useAgentWorkOrdersStore.getState().liveProgress[workOrderId]; + expect(progress.currentStep).toBe('planning'); + expect(progress.stepNumber).toBe(2); + expect(progress.progressPct).toBe(40); + }); +}); +``` + +--- + +## Performance Expectations + +### Current (With Polling) +- **HTTP Requests:** 140/min (3 active work orders) +- **Bandwidth:** 50-100KB/min (with ETags) +- **Latency:** Up to 3 second delay for updates +- **Client CPU:** Moderate (constant polling, re-renders) + +### After (With SSE + Zustand) +- **HTTP Requests:** ~14/min (only for mutations and initial loads) +- **SSE Connections:** 1-5 persistent connections +- **Bandwidth:** 5-10KB/min (events only, no 304 overhead) +- **Latency:** <100ms (instant SSE delivery) +- **Client CPU:** Lower (event-driven, selective re-renders) + +**Savings: 90% bandwidth reduction, 95% request reduction, instant updates** + +--- + +## Migration Risk Assessment + +### Low Risk +- ✅ UI Preferences slice (localStorage → Zustand persist) +- ✅ Modals slice (no external dependencies) +- ✅ SSE logs integration (already built, just use it) + +### Medium Risk +- ⚠️ URL sync with Zustand (needs careful testing) +- ⚠️ SSE connection management (need proper cleanup) +- ⚠️ Selective subscriptions (team must learn pattern) + +### High Risk (Don't Do) +- ❌ Replacing TanStack Query with Zustand (don't do this!) +- ❌ Global state for all forms (overkill) +- ❌ Putting row expansion in global state (terrible idea) + +--- + +## Decision Matrix: What Goes Where? + +| State Type | Current | Should Be | Reason | +|------------|---------|-----------|--------| +| layoutMode | useState + localStorage | Zustand (persisted) | Automatic persistence, global access | +| sidebarExpanded | useState | Zustand (persisted) | Should persist across reloads | +| showAddRepoModal | useState | Zustand (not persisted) | Enable opening from anywhere | +| editingRepository | useState | Zustand (not persisted) | Context for edit modal | +| searchQuery | useState | Zustand (persisted) | Persist search across navigation | +| selectedRepositoryId | URL params | Zustand + URL sync (persisted) | Dual source: Zustand cache + URL truth | +| repositories (server) | TanStack Query | TanStack Query | Perfect for server state | +| workOrders (server) | TanStack Query | TanStack Query + SSE overlay | Initial load (Query), updates (SSE) | +| repositoryUrl (form) | useState in modal | useState in modal | Simple, resets on close | +| selectedSteps (form) | useState in modal | useState in modal | Simple, resets on close | +| isExpanded (row) | useState per row | useState per row | Component-specific | +| SSE connections | useWorkOrderLogs hook | Zustand SSE slice | Centralized management | +| logs (from SSE) | useWorkOrderLogs hook | Zustand SSE slice | Share across components | +| progress (parsed logs) | useLogStats hook | Zustand SSE slice | Auto-parse on event | + +--- + +## Code Examples + +### Before: AgentWorkOrdersView (Current) +```typescript +export function AgentWorkOrdersView() { + // 8 separate useState calls + const [layoutMode, setLayoutMode] = useState(getInitialLayoutMode); + const [sidebarExpanded, setSidebarExpanded] = useState(true); + const [showAddRepoModal, setShowAddRepoModal] = useState(false); + const [showEditRepoModal, setShowEditRepoModal] = useState(false); + const [editingRepository, setEditingRepository] = useState(null); + const [showNewWorkOrderModal, setShowNewWorkOrderModal] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const selectedRepositoryId = searchParams.get("repo") || undefined; + + // Helper functions (20+ lines) + const updateLayoutMode = (mode: LayoutMode) => { + setLayoutMode(mode); + saveLayoutMode(mode); // Manual localStorage + }; + + const handleEditRepository = (repository: ConfiguredRepository) => { + setEditingRepository(repository); + setShowEditRepoModal(true); + }; + + // Server data (polls every 3s) + const { data: repositories = [] } = useRepositories(); + const { data: workOrders = [] } = useWorkOrders(); // Polling! + + // ... 400 lines total +} +``` + +--- + +### After: AgentWorkOrdersView (With Zustand) +```typescript +export function AgentWorkOrdersView() { + const [searchParams, setSearchParams] = useSearchParams(); + + // Zustand UI Preferences + const layoutMode = useAgentWorkOrdersStore((s) => s.layoutMode); + const sidebarExpanded = useAgentWorkOrdersStore((s) => s.sidebarExpanded); + const setLayoutMode = useAgentWorkOrdersStore((s) => s.setLayoutMode); + const toggleSidebar = useAgentWorkOrdersStore((s) => s.toggleSidebar); + + // Zustand Modals + const showAddRepoModal = useAgentWorkOrdersStore((s) => s.showAddRepoModal); + const showEditRepoModal = useAgentWorkOrdersStore((s) => s.showEditRepoModal); + const showCreateWorkOrderModal = useAgentWorkOrdersStore((s) => s.showCreateWorkOrderModal); + const editingRepository = useAgentWorkOrdersStore((s) => s.editingRepository); + const openAddRepoModal = useAgentWorkOrdersStore((s) => s.openAddRepoModal); + const openEditRepoModal = useAgentWorkOrdersStore((s) => s.openEditRepoModal); + const closeEditRepoModal = useAgentWorkOrdersStore((s) => s.closeEditRepoModal); + const openCreateWorkOrderModal = useAgentWorkOrdersStore((s) => s.openCreateWorkOrderModal); + const closeCreateWorkOrderModal = useAgentWorkOrdersStore((s) => s.closeCreateWorkOrderModal); + + // Zustand Filters + const searchQuery = useAgentWorkOrdersStore((s) => s.searchQuery); + const selectedRepositoryId = useAgentWorkOrdersStore((s) => s.selectedRepositoryId); + const setSearchQuery = useAgentWorkOrdersStore((s) => s.setSearchQuery); + const selectRepository = useAgentWorkOrdersStore((s) => s.selectRepository); + + // Sync Zustand with URL params (bidirectional) + useEffect(() => { + const urlRepoId = searchParams.get("repo") || undefined; + if (urlRepoId !== selectedRepositoryId) { + selectRepository(urlRepoId, setSearchParams); + } + }, [searchParams]); + + // Server data (TanStack Query - NO POLLING after Phase 4) + const { data: repositories = [] } = useRepositories(); + const { data: cachedWorkOrders = [] } = useWorkOrders({ refetchInterval: false }); + + // Live updates from SSE (Phase 4) + const liveWorkOrders = useAgentWorkOrdersStore((s) => s.liveWorkOrders); + const workOrders = cachedWorkOrders.map((wo) => ({ + ...wo, + ...(liveWorkOrders[wo.agent_work_order_id] || {}), // SSE overrides + })); + + // ... ~360 lines total (-40 lines) +} +``` + +**Changes:** +- ✅ No manual localStorage (automatic via persist) +- ✅ No helper functions (actions are in store) +- ✅ Can open modals from anywhere +- ✅ No polling (SSE provides updates) +- ❌ More verbose selectors (but clearer intent) + +--- + +## Final Architecture Diagram + +``` +┌─────────────────────────────────────────────────────────────┐ +│ AgentWorkOrdersView │ +│ ┌────────────────┐ ┌──────────────┐ ┌────────────────┐ │ +│ │ Zustand Store │ │ TanStack │ │ Components │ │ +│ │ │ │ Query │ │ │ │ +│ │ ├─ UI Prefs │ │ │ │ ├─ RepoCard │ │ +│ │ ├─ Modals │ │ ├─ Repos │ │ ├─ WorkOrder │ │ +│ │ ├─ Filters │ │ ├─ WorkOrders│ │ │ Table │ │ +│ │ └─ SSE │ │ └─ Mutations │ │ └─ Modals │ │ +│ └────────────────┘ └──────────────┘ └────────────────┘ │ +│ │ │ │ │ +│ └───────────────────┴───────────────────┘ │ +│ │ │ +└─────────────────────────────┼───────────────────────────────┘ + │ + ┌─────────────┴─────────────┐ + │ │ + ┌──────▼──────┐ ┌──────▼──────┐ + │ Backend │ │ Backend │ + │ REST API │ │ SSE Stream │ + │ │ │ │ + │ GET /repos │ │ GET /logs/ │ + │ POST /wo │ │ stream │ + │ PATCH /repo │ │ │ + └─────────────┘ └─────────────┘ +``` + +**Data Flow:** +1. **Initial Load:** TanStack Query → REST API → Cache +2. **Real-Time Updates:** SSE Stream → Zustand SSE Slice → Components +3. **User Actions:** Component → Zustand Action → TanStack Query Mutation → REST API +4. **UI State:** Component → Zustand Selector → Render + +--- + +## Summary + +### Use Zustand For: +1. ✅ **UI Preferences** (layoutMode, sidebarExpanded) - Persisted +2. ✅ **Modal State** (visibility, editing context) - NOT persisted +3. ✅ **Filter State** (search, selected repo) - Persisted +4. ✅ **SSE Management** (connections, live data parsing) - NOT persisted + +### Use Zustand Slices For: +1. ✅ **Modals** - Clean separation, no prop drilling +2. ✅ **UI Preferences** - Persistence with minimal code +3. ✅ **SSE** - Connection lifecycle management +4. ⚠️ **Forms** - Only if complex validation or "save draft" needed +5. ❌ **Ephemeral UI** - Keep local useState for row expansion, etc. + +### Keep TanStack Query For: +1. ✅ **Server Data** - Initial loads, caching, mutations +2. ✅ **Optimistic Updates** - TanStack Query handles this perfectly +3. ✅ **Request Deduplication** - Built-in +4. ✅ **Background Refetch** - For completed work orders (no SSE needed) + +### Keep Local useState For: +1. ✅ **Simple Forms** - Reset on close, no sharing needed +2. ✅ **Ephemeral UI** - Row expansion, animation triggers +3. ✅ **Component-Specific** - showLogs toggle in RealTimeStats + +--- + +## Expected Outcomes + +### Code Metrics +- **Current:** 4,400 lines +- **After Phase 4:** 4,890 lines (+490 lines / +11%) +- **Net Change:** +350 Zustand, +200 SSE, -60 removed boilerplate + +### Performance Metrics +- **HTTP Requests:** 140/min → 14/min (-90%) +- **Bandwidth:** 50-100KB/min → 5-10KB/min (-90%) +- **Update Latency:** 3 seconds → <100ms (-97%) +- **Client Re-renders:** Reduced (selective subscriptions) + +### Developer Experience +- ✅ No manual localStorage management +- ✅ No prop drilling for modals +- ✅ Truly real-time updates (SSE) +- ✅ Better debugging (Zustand DevTools) +- ⚠️ Slightly more verbose (selective subscriptions) +- ⚠️ Learning curve (Zustand patterns, SSE lifecycle) + +**Verdict: Net positive - real-time architecture is worth the 11% code increase** + +--- + +## Next Steps + +**DO NOT IMPLEMENT YET - This document is the reference for creating a PRP.** + +When creating the PRP: +1. Reference this document for architecture decisions +2. Follow the 5-phase implementation plan +3. Include all anti-patterns as validation gates +4. Add comprehensive test requirements +5. Document Zustand + SSE patterns for other features to follow + +This is a **pilot feature** - success here validates the pattern for Knowledge Base, Projects, and Settings. diff --git a/PRPs/ai_docs/ARCHITECTURE.md b/PRPs/ai_docs/ARCHITECTURE.md index eb3a7f81..8e2ec144 100644 --- a/PRPs/ai_docs/ARCHITECTURE.md +++ b/PRPs/ai_docs/ARCHITECTURE.md @@ -67,6 +67,11 @@ components/ # Legacy components (migrating) **Purpose**: Document processing, code analysis, project generation **Port**: 8052 +### Agent Work Orders (Optional) +**Location**: `python/src/agent_work_orders/` +**Purpose**: Workflow execution engine using Claude Code CLI +**Port**: 8053 + ## API Structure ### RESTful Endpoints diff --git a/PRPs/ai_docs/ZUSTAND_STATE_MANAGEMENT.md b/PRPs/ai_docs/ZUSTAND_STATE_MANAGEMENT.md new file mode 100644 index 00000000..8d9becfa --- /dev/null +++ b/PRPs/ai_docs/ZUSTAND_STATE_MANAGEMENT.md @@ -0,0 +1,269 @@ +Zustand v4 AI Coding Assistant Standards + +Purpose + +These guidelines define how an AI coding assistant should generate, refactor, and reason about Zustand (v4) state management code. They serve as enforceable standards to ensure clarity, consistency, maintainability, and performance across all code suggestions. + +⸻ + +1. General Rules + • Use TypeScript for all Zustand stores. + • All stores must be defined with the create() function from Zustand v4. + • State must be immutable; never mutate arrays or objects directly. + • Use functional updates with set((state) => ...) whenever referencing existing state. + • Never use useStore.getState() inside React render logic. + +⸻ + +2. Store Creation Rules + +Do: + +import { create } from 'zustand'; + +type CounterStore = { + count: number; + increment: () => void; + reset: () => void; +}; + +export const useCounterStore = create((set) => ({ + count: 0, + increment: () => set((state) => ({ count: state.count + 1 })), + reset: () => set({ count: 0 }) +})); + +Don’t: + • Define stores inline within components. + • Create multiple stores for related state when a single one suffices. + • Nest stores inside hooks or conditional logic. + +Naming conventions: + • Hook: useStore (e.g., useUserStore, useThemeStore). + • File: same as hook (e.g., useUserStore.ts). + +⸻ + +3. Store Organization Rules + • Each feature (e.g., agent-work-orders, knowledge, settings, etc..) should have its own store file. + • Combine complex stores using slices, not nested state. + • Use middleware (persist, devtools, immer) only when necessary. + +Example structure: + +src/features/knowledge/state/ + ├── knowledgeStore.ts + └── slices/ #If necessary + ├── nameSlice.ts #a name that represents the slice if needed + + +⸻ + +4. Selector and Subscription Rules + +Core Principle: Components should subscribe only to the exact slice of state they need. + +Do: + +const count = useCounterStore((s) => s.count); +const increment = useCounterStore((s) => s.increment); + +Don’t: + +const { count, increment } = useCounterStore(); // ❌ Causes unnecessary re-renders + +Additional rules: + • Use shallow comparison (shallow) if selecting multiple fields. + • Avoid subscribing to derived values that can be computed locally. + +⸻ + +5. Middleware and Side Effects + +Allowed middleware: persist, devtools, immer, subscribeWithSelector. + +Rules: + • Never persist volatile or sensitive data (e.g., tokens, temp state). + • Configure partialize to persist only essential state. + • Guard devtools with environment checks. + +Example: + +import { create } from 'zustand'; +import { persist, devtools } from 'zustand/middleware'; + +export const useSettingsStore = create( + devtools( + persist( + (set) => ({ + theme: 'light', + toggleTheme: () => set((s) => ({ theme: s.theme === 'light' ? 'dark' : 'light' })) + }), + { + name: 'settings-store', + partialize: (state) => ({ theme: state.theme }) + } + ) + ) +); + + +⸻ + +6. Async Logic Rules + • Async actions should be defined inside the store. + • Avoid direct useEffect calls that depend on store state. + +Do: + +fetchData: async () => { + const data = await api.getData(); + set({ data }); +} + +Don’t: + +useEffect(() => { + useStore.getState().fetchData(); // ❌ Side effect in React hook +}, []); + + +⸻ + +7. Anti-Patterns + +❌ Anti-Pattern 🚫 Reason +Subscribing to full store Causes unnecessary re-renders +Inline store creation in component Breaks referential integrity +Mutating state directly Zustand expects immutability +Business logic inside components Should live in store actions +Using store for local-only UI state Clutters global state +Multiple independent stores for one domain Increases complexity + + +⸻ + +8. Testing Rules + • Each store must be testable as a pure function. + • Tests should verify: initial state, action side effects, and immutability. + +Example Jest test: + +import { useCounterStore } from '../state/useCounterStore'; + +test('increment increases count', () => { + const { increment, count } = useCounterStore.getState(); + increment(); + expect(useCounterStore.getState().count).toBe(count + 1); +}); + + +⸻ + +9. Documentation Rules + • Every store file must include: + • Top-level JSDoc summarizing store purpose. + • Type definitions for state and actions. + • Examples for consumption patterns. + • Maintain a STATE_GUIDELINES.md index in the repo root linking all store docs. + +⸻ + +10. Enforcement Summary (AI Assistant Logic) + +When generating Zustand code: + • ALWAYS define stores with create() at module scope. + • NEVER create stores inside React components. + • ALWAYS use selectors in components. + • AVOID getState() in render logic. + • PREFER shallow comparison for multiple subscriptions. + • LIMIT middleware to proven cases (persist, devtools, immer). + • TEST every action in isolation. + • DOCUMENT store purpose, shape, and actions. + +⸻ +# Zustand v3 → v4 Summary (for AI Coding Assistants) + +## Overview +Zustand v4 introduced a few key syntax and type changes focused on improving TypeScript inference, middleware chaining, and internal consistency. +All existing concepts (store creation, selectors, middleware, subscriptions) remain — only the *patterns* and *type structure* changed. + +--- + +## Core Concept Changes +- **Curried Store Creation:** + `create()` now expects a *curried call* form when using generics or middleware. + The previous single-call pattern is deprecated. + +- **TypeScript Inference Improvements:** + v4’s curried syntax provides stronger type inference for complex stores and middleware combinations. + +- **Stricter Generic Typing:** + Functions like `set`, `get`, and the store API have tighter TypeScript types. + Any implicit `any` usage or loosely typed middleware will now error until corrected. + +--- + +## Middleware Updates +- Middleware is still supported but must be imported from subpaths (e.g., `zustand/middleware/immer`). +- The structure of most built-in middlewares (persist, devtools, immer, subscribeWithSelector) remains identical. +- Chaining multiple middlewares now depends on the curried `create` syntax for correct type inference. + +--- + +## Persistence and Migration +- `persist` behavior is unchanged functionally, but TypeScript typing for the `migrate` function now defines the input state as `unknown`. + You must assert or narrow this type when using TypeScript. +- The `name`, `version`, and other options are unchanged. + +--- + +## Type Adjustments +- The `set` function now includes a `replace` parameter for full state replacement. +- `get` and `api` generics are explicitly typed and must align with the store definition. +- Custom middleware and typed stores may need to specify generic parameters to avoid inference gaps. + +--- + +## Behavior and API Consistency +- Core APIs like `getState()`, `setState()`, and `subscribe()` are still valid. +- Hook usage (`useStore(state => state.value)`) is identical. +- Differences are primarily at compile time (typing), not runtime. + +--- + +## Migration/Usage Implications +For AI agents generating Zustand code: +- Always use the **curried `create()(…)`** pattern when defining stores. +- Always import middleware from `zustand/middleware/...`. +- Expect `set`, `get`, and `api` to have stricter typings. +- Assume `migrate` in persistence returns `unknown` and must be asserted. +- Avoid any v3-style `create(fn)` calls. +- Middleware chaining depends on the curried syntax — never use nested functions without it. + +--- + +## Reference Behavior +- Functional concepts are unchanged: stores, actions, and reactivity all behave the same. +- Only the declaration pattern and TypeScript inference system differ. + +--- + +## Summary +| Area | Zustand v3 | Zustand v4 | +|------|-------------|------------| +| Store creation | Single function call | Curried two-step syntax | +| TypeScript inference | Looser | Stronger, middleware-aware | +| Middleware imports | Flat path | Sub-path imports | +| Migrate typing | `any` | `unknown` | +| API methods | Same | Same, stricter typing | +| Runtime behavior | Same | Same | + +--- + +## Key Principle for Code Generation +> “If defining a store, always use the curried `create()` syntax, import middleware from subpaths, and respect stricter generics. All functional behavior remains identical to v3.” + +--- + +**Recommended Source:** [Zustand v4 Migration Guide – Official Docs](https://zustand.docs.pmnd.rs/migrations/migrating-to-v4) diff --git a/PRPs/ai_docs/cc_cli_ref.md b/PRPs/ai_docs/cc_cli_ref.md new file mode 100644 index 00000000..78572716 --- /dev/null +++ b/PRPs/ai_docs/cc_cli_ref.md @@ -0,0 +1,89 @@ +# CLI reference + +> Complete reference for Claude Code command-line interface, including commands and flags. + +## CLI commands + +| Command | Description | Example | +| :--------------------------------- | :--------------------------------------------- | :----------------------------------------------------------------- | +| `claude` | Start interactive REPL | `claude` | +| `claude "query"` | Start REPL with initial prompt | `claude "explain this project"` | +| `claude -p "query"` | Query via SDK, then exit | `claude -p "explain this function"` | +| `cat file \| claude -p "query"` | Process piped content | `cat logs.txt \| claude -p "explain"` | +| `claude -c` | Continue most recent conversation | `claude -c` | +| `claude -c -p "query"` | Continue via SDK | `claude -c -p "Check for type errors"` | +| `claude -r "" "query"` | Resume session by ID | `claude -r "abc123" "Finish this PR"` | +| `claude update` | Update to latest version | `claude update` | +| `claude mcp` | Configure Model Context Protocol (MCP) servers | See the [Claude Code MCP documentation](/en/docs/claude-code/mcp). | + +## CLI flags + +Customize Claude Code's behavior with these command-line flags: + +| Flag | Description | Example | +| :------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------- | +| `--add-dir` | Add additional working directories for Claude to access (validates each path exists as a directory) | `claude --add-dir ../apps ../lib` | +| `--agents` | Define custom [subagents](/en/docs/claude-code/sub-agents) dynamically via JSON (see below for format) | `claude --agents '{"reviewer":{"description":"Reviews code","prompt":"You are a code reviewer"}}'` | +| `--allowedTools` | A list of tools that should be allowed without prompting the user for permission, in addition to [settings.json files](/en/docs/claude-code/settings) | `"Bash(git log:*)" "Bash(git diff:*)" "Read"` | +| `--disallowedTools` | A list of tools that should be disallowed without prompting the user for permission, in addition to [settings.json files](/en/docs/claude-code/settings) | `"Bash(git log:*)" "Bash(git diff:*)" "Edit"` | +| `--print`, `-p` | Print response without interactive mode (see [SDK documentation](/en/docs/claude-code/sdk) for programmatic usage details) | `claude -p "query"` | +| `--append-system-prompt` | Append to system prompt (only with `--print`) | `claude --append-system-prompt "Custom instruction"` | +| `--output-format` | Specify output format for print mode (options: `text`, `json`, `stream-json`) | `claude -p "query" --output-format json` | +| `--input-format` | Specify input format for print mode (options: `text`, `stream-json`) | `claude -p --output-format json --input-format stream-json` | +| `--include-partial-messages` | Include partial streaming events in output (requires `--print` and `--output-format=stream-json`) | `claude -p --output-format stream-json --include-partial-messages "query"` | +| `--verbose` | Enable verbose logging, shows full turn-by-turn output (helpful for debugging in both print and interactive modes) | `claude --verbose` | +| `--max-turns` | Limit the number of agentic turns in non-interactive mode | `claude -p --max-turns 3 "query"` | +| `--model` | Sets the model for the current session with an alias for the latest model (`sonnet` or `opus`) or a model's full name | `claude --model claude-sonnet-4-5-20250929` | +| `--permission-mode` | Begin in a specified [permission mode](iam#permission-modes) | `claude --permission-mode plan` | +| `--permission-prompt-tool` | Specify an MCP tool to handle permission prompts in non-interactive mode | `claude -p --permission-prompt-tool mcp_auth_tool "query"` | +| `--resume` | Resume a specific session by ID, or by choosing in interactive mode | `claude --resume abc123 "query"` | +| `--continue` | Load the most recent conversation in the current directory | `claude --continue` | +| `--dangerously-skip-permissions` | Skip permission prompts (use with caution) | `claude --dangerously-skip-permissions` | + + + The `--output-format json` flag is particularly useful for scripting and + automation, allowing you to parse Claude's responses programmatically. + + +### Agents flag format + +The `--agents` flag accepts a JSON object that defines one or more custom subagents. Each subagent requires a unique name (as the key) and a definition object with the following fields: + +| Field | Required | Description | +| :------------ | :------- | :-------------------------------------------------------------------------------------------------------------- | +| `description` | Yes | Natural language description of when the subagent should be invoked | +| `prompt` | Yes | The system prompt that guides the subagent's behavior | +| `tools` | No | Array of specific tools the subagent can use (e.g., `["Read", "Edit", "Bash"]`). If omitted, inherits all tools | +| `model` | No | Model alias to use: `sonnet`, `opus`, or `haiku`. If omitted, uses the default subagent model | + +Example: + +```bash theme={null} +claude --agents '{ + "code-reviewer": { + "description": "Expert code reviewer. Use proactively after code changes.", + "prompt": "You are a senior code reviewer. Focus on code quality, security, and best practices.", + "tools": ["Read", "Grep", "Glob", "Bash"], + "model": "sonnet" + }, + "debugger": { + "description": "Debugging specialist for errors and test failures.", + "prompt": "You are an expert debugger. Analyze errors, identify root causes, and provide fixes." + } +}' +``` + +For more details on creating and using subagents, see the [subagents documentation](/en/docs/claude-code/sub-agents). + +For detailed information about print mode (`-p`) including output formats, +streaming, verbose logging, and programmatic usage, see the +[SDK documentation](/en/docs/claude-code/sdk). + +## See also + +- [Interactive mode](/en/docs/claude-code/interactive-mode) - Shortcuts, input modes, and interactive features +- [Slash commands](/en/docs/claude-code/slash-commands) - Interactive session commands +- [Quickstart guide](/en/docs/claude-code/quickstart) - Getting started with Claude Code +- [Common workflows](/en/docs/claude-code/common-workflows) - Advanced workflows and patterns +- [Settings](/en/docs/claude-code/settings) - Configuration options +- [SDK documentation](/en/docs/claude-code/sdk) - Programmatic usage and integrations diff --git a/README.md b/README.md index c579233f..22e8ecba 100644 --- a/README.md +++ b/README.md @@ -204,12 +204,13 @@ The reset script safely removes all tables, functions, triggers, and policies wi ### Core Services -| Service | Container Name | Default URL | Purpose | -| ------------------ | -------------- | --------------------- | --------------------------------- | -| **Web Interface** | archon-ui | http://localhost:3737 | Main dashboard and controls | -| **API Service** | archon-server | http://localhost:8181 | Web crawling, document processing | -| **MCP Server** | archon-mcp | http://localhost:8051 | Model Context Protocol interface | -| **Agents Service** | archon-agents | http://localhost:8052 | AI/ML operations, reranking | +| Service | Container Name | Default URL | Purpose | +| -------------------------- | -------------------------- | --------------------- | ------------------------------------------ | +| **Web Interface** | archon-ui | http://localhost:3737 | Main dashboard and controls | +| **API Service** | archon-server | http://localhost:8181 | Web crawling, document processing | +| **MCP Server** | archon-mcp | http://localhost:8051 | Model Context Protocol interface | +| **Agents Service** | archon-agents | http://localhost:8052 | AI/ML operations, reranking | +| **Agent Work Orders** *(optional)* | archon-agent-work-orders | http://localhost:8053 | Workflow execution with Claude Code CLI | ## Upgrading @@ -293,12 +294,13 @@ Archon uses true microservices architecture with clear separation of concerns: ### Service Responsibilities -| Service | Location | Purpose | Key Features | -| -------------- | -------------------- | ---------------------------- | ------------------------------------------------------------------ | -| **Frontend** | `archon-ui-main/` | Web interface and dashboard | React, TypeScript, TailwindCSS, Socket.IO client | -| **Server** | `python/src/server/` | Core business logic and APIs | FastAPI, service layer, Socket.IO broadcasts, all ML/AI operations | -| **MCP Server** | `python/src/mcp/` | MCP protocol interface | Lightweight HTTP wrapper, MCP tools, session management | -| **Agents** | `python/src/agents/` | PydanticAI agent hosting | Document and RAG agents, streaming responses | +| Service | Location | Purpose | Key Features | +| ------------------------ | ------------------------------ | -------------------------------- | ------------------------------------------------------------------ | +| **Frontend** | `archon-ui-main/` | Web interface and dashboard | React, TypeScript, TailwindCSS, Socket.IO client | +| **Server** | `python/src/server/` | Core business logic and APIs | FastAPI, service layer, Socket.IO broadcasts, all ML/AI operations | +| **MCP Server** | `python/src/mcp/` | MCP protocol interface | Lightweight HTTP wrapper, MCP tools, session management | +| **Agents** | `python/src/agents/` | PydanticAI agent hosting | Document and RAG agents, streaming responses | +| **Agent Work Orders** *(optional)* | `python/src/agent_work_orders/` | Workflow execution engine | Claude Code CLI automation, repository management, SSE updates | ### Communication Patterns @@ -321,7 +323,8 @@ By default, Archon services run on the following ports: - **archon-ui**: 3737 - **archon-server**: 8181 - **archon-mcp**: 8051 -- **archon-agents**: 8052 +- **archon-agents**: 8052 (optional) +- **archon-agent-work-orders**: 8053 (optional) - **archon-docs**: 3838 (optional) ### Changing Ports @@ -334,6 +337,7 @@ ARCHON_UI_PORT=3737 ARCHON_SERVER_PORT=8181 ARCHON_MCP_PORT=8051 ARCHON_AGENTS_PORT=8052 +AGENT_WORK_ORDERS_PORT=8053 ARCHON_DOCS_PORT=3838 ``` diff --git a/archon-ui-main/.env.example b/archon-ui-main/.env.example new file mode 100644 index 00000000..284c8ea7 --- /dev/null +++ b/archon-ui-main/.env.example @@ -0,0 +1,13 @@ +# Frontend Environment Configuration + +# Agent Work Orders Service (Optional) +# Only set if agent work orders service runs on different host/port than main server +# Default: Uses proxy through main server at /api/agent-work-orders +# Set to the base URL (without /api/agent-work-orders path) +# VITE_AGENT_WORK_ORDERS_URL=http://localhost:8053 + +# Development Tools +# Show TanStack Query DevTools (for developers only) +# Set to "true" to enable the DevTools panel in bottom right corner +# Defaults to "false" for end users +VITE_SHOW_DEVTOOLS=false diff --git a/archon-ui-main/package-lock.json b/archon-ui-main/package-lock.json index 37b3e9a7..74f7568e 100644 --- a/archon-ui-main/package-lock.json +++ b/archon-ui-main/package-lock.json @@ -38,7 +38,8 @@ "react-markdown": "^10.1.0", "react-router-dom": "^6.26.2", "tailwind-merge": "latest", - "zod": "^3.25.46" + "zod": "^3.25.46", + "zustand": "^5.0.8" }, "devDependencies": { "@biomejs/biome": "2.2.2", @@ -11844,6 +11845,35 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/archon-ui-main/package.json b/archon-ui-main/package.json index 576b78ae..9e1b4e64 100644 --- a/archon-ui-main/package.json +++ b/archon-ui-main/package.json @@ -58,7 +58,8 @@ "react-markdown": "^10.1.0", "react-router-dom": "^6.26.2", "tailwind-merge": "latest", - "zod": "^3.25.46" + "zod": "^3.25.46", + "zustand": "^5.0.8" }, "devDependencies": { "@biomejs/biome": "2.2.2", diff --git a/archon-ui-main/src/App.tsx b/archon-ui-main/src/App.tsx index 36e0d375..acb88734 100644 --- a/archon-ui-main/src/App.tsx +++ b/archon-ui-main/src/App.tsx @@ -14,6 +14,8 @@ import { SettingsProvider, useSettings } from './contexts/SettingsContext'; import { TooltipProvider } from './features/ui/primitives/tooltip'; import { ProjectPage } from './pages/ProjectPage'; import StyleGuidePage from './pages/StyleGuidePage'; +import { AgentWorkOrdersPage } from './pages/AgentWorkOrdersPage'; +import { AgentWorkOrderDetailPage } from './pages/AgentWorkOrderDetailPage'; import { DisconnectScreenOverlay } from './components/DisconnectScreenOverlay'; import { ErrorBoundaryWithBugReport } from './components/bug-report/ErrorBoundaryWithBugReport'; import { MigrationBanner } from './components/ui/MigrationBanner'; @@ -22,7 +24,7 @@ import { useMigrationStatus } from './hooks/useMigrationStatus'; const AppRoutes = () => { - const { projectsEnabled, styleGuideEnabled } = useSettings(); + const { projectsEnabled, styleGuideEnabled, agentWorkOrdersEnabled } = useSettings(); return ( @@ -43,6 +45,14 @@ const AppRoutes = () => { ) : ( } /> )} + {agentWorkOrdersEnabled ? ( + <> + } /> + } /> + + ) : ( + } /> + )} ); }; diff --git a/archon-ui-main/src/components/layout/Navigation.tsx b/archon-ui-main/src/components/layout/Navigation.tsx index 3547b5fb..c1996ff7 100644 --- a/archon-ui-main/src/components/layout/Navigation.tsx +++ b/archon-ui-main/src/components/layout/Navigation.tsx @@ -1,4 +1,4 @@ -import { BookOpen, Palette, Settings } from "lucide-react"; +import { BookOpen, Bot, Palette, Settings } from "lucide-react"; import type React from "react"; import { Link, useLocation } from "react-router-dom"; // TEMPORARY: Use old SettingsContext until settings are migrated @@ -24,7 +24,7 @@ interface NavigationProps { */ export function Navigation({ className }: NavigationProps) { const location = useLocation(); - const { projectsEnabled, styleGuideEnabled } = useSettings(); + const { projectsEnabled, styleGuideEnabled, agentWorkOrdersEnabled } = useSettings(); // Navigation items configuration const navigationItems: NavigationItem[] = [ @@ -34,6 +34,12 @@ export function Navigation({ className }: NavigationProps) { label: "Knowledge Base", enabled: true, }, + { + path: "/agent-work-orders", + icon: , + label: "Agent Work Orders", + enabled: agentWorkOrdersEnabled, + }, { path: "/mcp", icon: ( diff --git a/archon-ui-main/src/components/settings/FeaturesSection.tsx b/archon-ui-main/src/components/settings/FeaturesSection.tsx index 1f410baf..9740520d 100644 --- a/archon-ui-main/src/components/settings/FeaturesSection.tsx +++ b/archon-ui-main/src/components/settings/FeaturesSection.tsx @@ -14,10 +14,16 @@ export const FeaturesSection = () => { setTheme } = useTheme(); const { showToast } = useToast(); - const { styleGuideEnabled, setStyleGuideEnabled: setStyleGuideContext } = useSettings(); + const { + styleGuideEnabled, + setStyleGuideEnabled: setStyleGuideContext, + agentWorkOrdersEnabled, + setAgentWorkOrdersEnabled: setAgentWorkOrdersContext + } = useSettings(); const isDarkMode = theme === 'dark'; const [projectsEnabled, setProjectsEnabled] = useState(true); const [styleGuideEnabledLocal, setStyleGuideEnabledLocal] = useState(styleGuideEnabled); + const [agentWorkOrdersEnabledLocal, setAgentWorkOrdersEnabledLocal] = useState(agentWorkOrdersEnabled); // Commented out for future release const [agUILibraryEnabled, setAgUILibraryEnabled] = useState(false); @@ -38,6 +44,10 @@ export const FeaturesSection = () => { setStyleGuideEnabledLocal(styleGuideEnabled); }, [styleGuideEnabled]); + useEffect(() => { + setAgentWorkOrdersEnabledLocal(agentWorkOrdersEnabled); + }, [agentWorkOrdersEnabled]); + const loadSettings = async () => { try { setLoading(true); @@ -224,6 +234,29 @@ export const FeaturesSection = () => { } }; + const handleAgentWorkOrdersToggle = async (checked: boolean) => { + if (loading) return; + + try { + setLoading(true); + setAgentWorkOrdersEnabledLocal(checked); + + // Update context which will save to backend + await setAgentWorkOrdersContext(checked); + + showToast( + checked ? 'Agent Work Orders Enabled' : 'Agent Work Orders Disabled', + checked ? 'success' : 'warning' + ); + } catch (error) { + console.error('Failed to update agent work orders setting:', error); + setAgentWorkOrdersEnabledLocal(!checked); + showToast('Failed to update agent work orders setting', 'error'); + } finally { + setLoading(false); + } + }; + return ( <>
@@ -298,6 +331,28 @@ export const FeaturesSection = () => {
+ {/* Agent Work Orders Toggle */} +
+
+

+ Agent Work Orders +

+

+ Enable automated development workflows with Claude Code CLI +

+
+
+ } + disabled={loading} + /> +
+
+ {/* COMMENTED OUT FOR FUTURE RELEASE - AG-UI Library Toggle */} {/*
diff --git a/archon-ui-main/src/contexts/SettingsContext.tsx b/archon-ui-main/src/contexts/SettingsContext.tsx index ff8f2264..40da7115 100644 --- a/archon-ui-main/src/contexts/SettingsContext.tsx +++ b/archon-ui-main/src/contexts/SettingsContext.tsx @@ -6,6 +6,8 @@ interface SettingsContextType { setProjectsEnabled: (enabled: boolean) => Promise; styleGuideEnabled: boolean; setStyleGuideEnabled: (enabled: boolean) => Promise; + agentWorkOrdersEnabled: boolean; + setAgentWorkOrdersEnabled: (enabled: boolean) => Promise; loading: boolean; refreshSettings: () => Promise; } @@ -27,16 +29,18 @@ interface SettingsProviderProps { export const SettingsProvider: React.FC = ({ children }) => { const [projectsEnabled, setProjectsEnabledState] = useState(true); const [styleGuideEnabled, setStyleGuideEnabledState] = useState(false); + const [agentWorkOrdersEnabled, setAgentWorkOrdersEnabledState] = useState(false); const [loading, setLoading] = useState(true); const loadSettings = async () => { try { setLoading(true); - // Load Projects and Style Guide settings - const [projectsResponse, styleGuideResponse] = await Promise.all([ + // Load Projects, Style Guide, and Agent Work Orders settings + const [projectsResponse, styleGuideResponse, agentWorkOrdersResponse] = await Promise.all([ credentialsService.getCredential('PROJECTS_ENABLED').catch(() => ({ value: undefined })), - credentialsService.getCredential('STYLE_GUIDE_ENABLED').catch(() => ({ value: undefined })) + credentialsService.getCredential('STYLE_GUIDE_ENABLED').catch(() => ({ value: undefined })), + credentialsService.getCredential('AGENT_WORK_ORDERS_ENABLED').catch(() => ({ value: undefined })) ]); if (projectsResponse.value !== undefined) { @@ -51,10 +55,17 @@ export const SettingsProvider: React.FC = ({ children }) setStyleGuideEnabledState(false); // Default to false } + if (agentWorkOrdersResponse.value !== undefined) { + setAgentWorkOrdersEnabledState(agentWorkOrdersResponse.value === 'true'); + } else { + setAgentWorkOrdersEnabledState(false); // Default to false + } + } catch (error) { console.error('Failed to load settings:', error); setProjectsEnabledState(true); setStyleGuideEnabledState(false); + setAgentWorkOrdersEnabledState(false); } finally { setLoading(false); } @@ -106,6 +117,27 @@ export const SettingsProvider: React.FC = ({ children }) } }; + const setAgentWorkOrdersEnabled = async (enabled: boolean) => { + try { + // Update local state immediately + setAgentWorkOrdersEnabledState(enabled); + + // Save to backend + await credentialsService.createCredential({ + key: 'AGENT_WORK_ORDERS_ENABLED', + value: enabled.toString(), + is_encrypted: false, + category: 'features', + description: 'Enable Agent Work Orders feature for automated development workflows' + }); + } catch (error) { + console.error('Failed to update agent work orders setting:', error); + // Revert on error + setAgentWorkOrdersEnabledState(!enabled); + throw error; + } + }; + const refreshSettings = async () => { await loadSettings(); }; @@ -115,6 +147,8 @@ export const SettingsProvider: React.FC = ({ children }) setProjectsEnabled, styleGuideEnabled, setStyleGuideEnabled, + agentWorkOrdersEnabled, + setAgentWorkOrdersEnabled, loading, refreshSettings }; diff --git a/archon-ui-main/src/features/agent-work-orders/components/AddRepositoryModal.tsx b/archon-ui-main/src/features/agent-work-orders/components/AddRepositoryModal.tsx new file mode 100644 index 00000000..8789b287 --- /dev/null +++ b/archon-ui-main/src/features/agent-work-orders/components/AddRepositoryModal.tsx @@ -0,0 +1,228 @@ +/** + * Add Repository Modal Component + * + * Modal for adding new configured repositories with GitHub verification. + * Two-column layout: Left (2/3) for form fields, Right (1/3) for workflow steps. + */ + +import { Loader2 } from "lucide-react"; +import { useState } from "react"; +import { Button } from "@/features/ui/primitives/button"; +import { Checkbox } from "@/features/ui/primitives/checkbox"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/features/ui/primitives/dialog"; +import { Input } from "@/features/ui/primitives/input"; +import { Label } from "@/features/ui/primitives/label"; +import { useCreateRepository } from "../hooks/useRepositoryQueries"; +import type { WorkflowStep } from "../types"; + +export interface AddRepositoryModalProps { + /** Whether modal is open */ + open: boolean; + + /** Callback to change open state */ + onOpenChange: (open: boolean) => void; +} + +/** + * All available workflow steps + */ +const WORKFLOW_STEPS: { value: WorkflowStep; label: string; description: string; dependsOn?: WorkflowStep[] }[] = [ + { value: "create-branch", label: "Create Branch", description: "Create a new git branch for isolated work" }, + { value: "planning", label: "Planning", description: "Generate implementation plan" }, + { value: "execute", label: "Execute", description: "Implement the planned changes" }, + { value: "prp-review", label: "Review/Fix", description: "Review implementation and fix issues", dependsOn: ["execute"] }, + { value: "commit", label: "Commit", description: "Commit changes to git", dependsOn: ["execute"] }, + { value: "create-pr", label: "Create PR", description: "Create pull request", dependsOn: ["commit"] }, +]; + +/** + * Default selected steps for new repositories + */ +const DEFAULT_STEPS: WorkflowStep[] = ["create-branch", "planning", "execute"]; + +export function AddRepositoryModal({ open, onOpenChange }: AddRepositoryModalProps) { + const [repositoryUrl, setRepositoryUrl] = useState(""); + const [selectedSteps, setSelectedSteps] = useState(DEFAULT_STEPS); + const [error, setError] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + const createRepository = useCreateRepository(); + + /** + * Reset form state + */ + const resetForm = () => { + setRepositoryUrl(""); + setSelectedSteps(DEFAULT_STEPS); + setError(""); + }; + + /** + * Toggle workflow step selection + * When unchecking a step, also uncheck steps that depend on it (cascade removal) + */ + const toggleStep = (step: WorkflowStep) => { + setSelectedSteps((prev) => { + if (prev.includes(step)) { + // Removing a step - also remove steps that depend on it + const stepsToRemove = new Set([step]); + + // Find all steps that transitively depend on the one being removed (cascade) + let changed = true; + while (changed) { + changed = false; + WORKFLOW_STEPS.forEach((s) => { + if (!stepsToRemove.has(s.value) && s.dependsOn?.some((dep) => stepsToRemove.has(dep))) { + stepsToRemove.add(s.value); + changed = true; + } + }); + } + + return prev.filter((s) => !stepsToRemove.has(s)); + } + return [...prev, step]; + }); + }; + + /** + * Check if a step is disabled based on dependencies + */ + const isStepDisabled = (step: (typeof WORKFLOW_STEPS)[number]): boolean => { + if (!step.dependsOn) return false; + return step.dependsOn.some((dep) => !selectedSteps.includes(dep)); + }; + + /** + * Handle form submission + */ + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + + // Validation + if (!repositoryUrl.trim()) { + setError("Repository URL is required"); + return; + } + if (!repositoryUrl.includes("github.com")) { + setError("Must be a GitHub repository URL"); + return; + } + if (selectedSteps.length === 0) { + setError("At least one workflow step must be selected"); + return; + } + + try { + setIsSubmitting(true); + await createRepository.mutateAsync({ + repository_url: repositoryUrl, + verify: true, + }); + + // Success - close modal and reset form + resetForm(); + onOpenChange(false); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to create repository"); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + Add Repository + + +
+
+ {/* Left Column (2/3 width) - Form Fields */} +
+ {/* Repository URL */} +
+ + setRepositoryUrl(e.target.value)} + aria-invalid={!!error} + /> +

+ GitHub repository URL. We'll verify access and extract metadata automatically. +

+
+ + {/* Info about auto-filled fields */} +
+

+ Auto-filled from GitHub: +

+
    +
  • Display Name (can be customized later via Edit)
  • +
  • Owner/Organization
  • +
  • Default Branch
  • +
+
+
+ + {/* Right Column (1/3 width) - Workflow Steps */} +
+ +
+ {WORKFLOW_STEPS.map((step) => { + const isSelected = selectedSteps.includes(step.value); + const isDisabled = isStepDisabled(step); + + return ( +
+ !isDisabled && toggleStep(step.value)} + disabled={isDisabled} + aria-label={step.label} + /> + +
+ ); + })} +
+

Commit and PR require Execute

+
+
+ + {/* Error Message */} + {error && ( +
+ {error} +
+ )} + + {/* Actions */} +
+ + +
+
+
+
+ ); +} diff --git a/archon-ui-main/src/features/agent-work-orders/components/CreateWorkOrderModal.tsx b/archon-ui-main/src/features/agent-work-orders/components/CreateWorkOrderModal.tsx new file mode 100644 index 00000000..251f9fc6 --- /dev/null +++ b/archon-ui-main/src/features/agent-work-orders/components/CreateWorkOrderModal.tsx @@ -0,0 +1,322 @@ +/** + * Create Work Order Modal Component + * + * Two-column modal for creating work orders with improved layout. + * Left column (2/3): Form fields for repository, request, issue + * Right column (1/3): Workflow steps selection + */ + +import { Loader2 } from "lucide-react"; +import { useEffect, useState } from "react"; +import { Button } from "@/features/ui/primitives/button"; +import { Checkbox } from "@/features/ui/primitives/checkbox"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/features/ui/primitives/dialog"; +import { Input, TextArea } from "@/features/ui/primitives/input"; +import { Label } from "@/features/ui/primitives/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/features/ui/primitives/select"; +import { useCreateWorkOrder } from "../hooks/useAgentWorkOrderQueries"; +import { useRepositories } from "../hooks/useRepositoryQueries"; +import { useAgentWorkOrdersStore } from "../state/agentWorkOrdersStore"; +import type { SandboxType, WorkflowStep } from "../types"; + +export interface CreateWorkOrderModalProps { + /** Whether modal is open */ + open: boolean; + + /** Callback to change open state */ + onOpenChange: (open: boolean) => void; +} + +/** + * All available workflow steps with dependency info + */ +const WORKFLOW_STEPS: { value: WorkflowStep; label: string; dependsOn?: WorkflowStep[] }[] = [ + { value: "create-branch", label: "Create Branch" }, + { value: "planning", label: "Planning" }, + { value: "execute", label: "Execute" }, + { value: "prp-review", label: "Review/Fix", dependsOn: ["execute"] }, + { value: "commit", label: "Commit Changes", dependsOn: ["execute"] }, + { value: "create-pr", label: "Create Pull Request", dependsOn: ["commit"] }, +]; + +export function CreateWorkOrderModal({ open, onOpenChange }: CreateWorkOrderModalProps) { + // Read preselected repository from Zustand store + const preselectedRepositoryId = useAgentWorkOrdersStore((s) => s.preselectedRepositoryId); + + const { data: repositories = [] } = useRepositories(); + const createWorkOrder = useCreateWorkOrder(); + + const [repositoryId, setRepositoryId] = useState(preselectedRepositoryId || ""); + const [repositoryUrl, setRepositoryUrl] = useState(""); + const [sandboxType, setSandboxType] = useState("git_worktree"); + const [userRequest, setUserRequest] = useState(""); + const [githubIssueNumber, setGithubIssueNumber] = useState(""); + const [selectedCommands, setSelectedCommands] = useState(["create-branch", "planning", "execute", "prp-review", "commit", "create-pr"]); + const [error, setError] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + /** + * Pre-populate form when repository is selected + */ + useEffect(() => { + if (preselectedRepositoryId) { + setRepositoryId(preselectedRepositoryId); + const repo = repositories.find((r) => r.id === preselectedRepositoryId); + if (repo) { + setRepositoryUrl(repo.repository_url); + setSandboxType(repo.default_sandbox_type); + setSelectedCommands(repo.default_commands as WorkflowStep[]); + } + } + }, [preselectedRepositoryId, repositories]); + + /** + * Handle repository selection change + */ + const handleRepositoryChange = (newRepositoryId: string) => { + setRepositoryId(newRepositoryId); + const repo = repositories.find((r) => r.id === newRepositoryId); + if (repo) { + setRepositoryUrl(repo.repository_url); + setSandboxType(repo.default_sandbox_type); + setSelectedCommands(repo.default_commands as WorkflowStep[]); + } + }; + + /** + * Toggle workflow step selection + * When unchecking a step, also uncheck steps that depend on it (cascade removal) + */ + const toggleStep = (step: WorkflowStep) => { + setSelectedCommands((prev) => { + if (prev.includes(step)) { + // Removing a step - also remove steps that depend on it + const stepsToRemove = new Set([step]); + + // Find all steps that transitively depend on the one being removed (cascade) + let changed = true; + while (changed) { + changed = false; + WORKFLOW_STEPS.forEach((s) => { + if (!stepsToRemove.has(s.value) && s.dependsOn?.some((dep) => stepsToRemove.has(dep))) { + stepsToRemove.add(s.value); + changed = true; + } + }); + } + + return prev.filter((s) => !stepsToRemove.has(s)); + } + return [...prev, step]; + }); + }; + + /** + * Check if a step is disabled based on dependencies + */ + const isStepDisabled = (step: (typeof WORKFLOW_STEPS)[number]): boolean => { + if (!step.dependsOn) return false; + return step.dependsOn.some((dep) => !selectedCommands.includes(dep)); + }; + + /** + * Reset form state + */ + const resetForm = () => { + setRepositoryId(preselectedRepositoryId || ""); + setRepositoryUrl(""); + setSandboxType("git_worktree"); + setUserRequest(""); + setGithubIssueNumber(""); + setSelectedCommands(["create-branch", "planning", "execute"]); + setError(""); + }; + + /** + * Handle form submission + */ + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + + // Validation + if (!repositoryUrl.trim()) { + setError("Repository URL is required"); + return; + } + if (userRequest.trim().length < 10) { + setError("Request must be at least 10 characters"); + return; + } + if (selectedCommands.length === 0) { + setError("At least one workflow step must be selected"); + return; + } + + try { + setIsSubmitting(true); + + // Sort selected commands by WORKFLOW_STEPS order before sending to backend + // This ensures correct execution order regardless of checkbox click order + const sortedCommands = WORKFLOW_STEPS + .filter(step => selectedCommands.includes(step.value)) + .map(step => step.value); + + await createWorkOrder.mutateAsync({ + repository_url: repositoryUrl, + sandbox_type: sandboxType, + user_request: userRequest, + github_issue_number: githubIssueNumber || undefined, + selected_commands: sortedCommands, + }); + + // Success - close modal and reset + resetForm(); + onOpenChange(false); + } catch (err) { + // Preserve error details by truncating long messages instead of hiding them + // Show up to 500 characters to capture important debugging information + // while keeping the UI readable + const maxLength = 500; + let userMessage = "Failed to create work order. Please try again."; + + if (err instanceof Error && err.message) { + if (err.message.length <= maxLength) { + userMessage = err.message; + } else { + // Truncate but preserve the start which often contains the most important details + userMessage = `${err.message.slice(0, maxLength)}... (truncated, ${err.message.length - maxLength} more characters)`; + } + } + + setError(userMessage); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + Create Work Order + + +
+
+ {/* Left Column (2/3 width) - Form Fields */} +
+ {/* Repository Selector */} +
+ + +
+ + {/* User Request */} +
+ +