diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..03360c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Ignore markdown docs except agent-prompts +*.md +!agent-prompts/*.md +!agent-prompts/**/*.md +# Binaries +starter +worker +poimen diff --git a/COMPLETE_T4_SUMMARY.md b/COMPLETE_T4_SUMMARY.md deleted file mode 100644 index a44b728..0000000 --- a/COMPLETE_T4_SUMMARY.md +++ /dev/null @@ -1,394 +0,0 @@ -# πŸŽ‰ **PROJECT COMPLETE: ALL 48 TASKS DELIVERED (T0-T4)** πŸŽ‰ - -## πŸ“Š FINAL COMPLETION STATUS - -``` -T0: 9/9 βœ… COMPLETE (100%) [Foundation] -T1: 8/8 βœ… COMPLETE (100%) [Production Hardening] -T2: 8/8 βœ… COMPLETE (100%) [Scale & Performance] -T3: 8/8 βœ… COMPLETE (100%) [Feature Expansion] -T4: 8/8 βœ… COMPLETE (100%) [Advanced Operations & Analytics] -──────────────────────────────────────────────── -TOTAL: 48/48 (100%) βœ… ALL MILESTONES COMPLETE -``` - ---- - -## πŸ† T4 ADVANCED OPERATIONS & ANALYTICS (8/8 βœ…) - -### T4.1: Real-Time Metrics Dashboard -- **Package**: `internal/dashboard` -- **Tests**: 13 -- **Features**: - - MetricsAggregator for time-series data collection - - Percentile calculations (p50, p95, p99) - - Min/max/average aggregation - - Metric-level statistics tracking - - Time-range queries - -### T4.2: Workflow Visualization & DAG Rendering -- **Package**: `internal/visualization` -- **Tests**: 12 -- **Features**: - - DAGRenderer for dependency graphs - - DOT format generation for Graphviz - - Critical path highlighting - - Topological sorting with Kahn's algorithm - - HTML visualization - - Parallel task grouping - -### T4.3: Advanced Search & Filtering -- **Package**: `internal/search` -- **Tests**: 18 -- **Features**: - - Full-text indexing with word-based lookup - - Filter by status, assignee, tag, date - - Regex pattern matching - - Saved filter persistence - - Case-insensitive search - - Multi-word search support - -### T4.4: Cost Tracking & Optimization -- **Package**: `internal/cost` -- **Tests**: 16 -- **Features**: - - LLM API cost tracking (per token) - - Git operation cost tracking - - Compute resource cost tracking (per duration) - - Cost aggregation by type/workflow - - Cost optimization recommendations - - Configurable rate settings - -### T4.5: Automated Alerting & Anomaly Detection -- **Package**: `internal/alerting` -- **Tests**: 12 -- **Features**: - - AlertManager for rule-based alerts - - Alert levels (warning, error, critical) - - Threshold-based alert triggering - - Alert history tracking - - Rule management - - Active alert queries - -### T4.6: Workflow Profiling & Bottleneck Analysis -- **Package**: `internal/profiling` -- **Tests**: 11 -- **Features**: - - WorkflowProfiler for execution metrics - - Per-task CPU/memory/duration tracking - - Identify slow tasks (top N slowest) - - High CPU/memory task detection - - Optimization suggestions - - Throughput calculation - -### T4.7: Multi-Cluster Orchestration -- **Package**: `internal/clusters` -- **Tests**: 13 -- **Features**: - - ClusterManager for K8s cluster management - - Register/unregister clusters - - Health checking - - Task allocation with load balancing - - Capacity tracking - - Find best cluster by available capacity - -### T4.8: Self-Deployment (Orchestrator Deploys Itself) -- **Package**: `internal/deployment` -- **Tests**: 12 -- **Features**: - - SelfDeployer for automated deployment - - Docker container build tracking - - Image push to registry - - K8s manifest generation - - Deployment status management - - Rollback support - ---- - -## πŸ“ˆ COMPLETE PROJECT STATISTICS - -### Code Metrics -| Metric | Value | -|--------|-------| -| Total Packages | 29 internal packages | -| Total Tests | 546 unit tests | -| Test Pass Rate | 100% | -| Lines of Code | ~28,000+ | -| Compilation Status | βœ… Zero errors | -| Git Commits | 40+ atomic commits | -| Branches Merged | 25 feature branches | - -### Test Breakdown -- T0: 50+ tests -- T1: 199 tests -- T2: 159 tests -- T3: 131 tests -- T4: 98 tests -- **Total**: 546+ tests βœ… - -### Packages by Milestone - -**T0-T1 (17 packages)**: -- approval, audit, batching, board -- cache, composition, config, dispatch -- external, graph, health, history -- indexing, judge, locking, logging -- metrics, pause, plugins, recovery -- templates, tuning - -**T4 New (8 packages)**: -- alerting, clusters, cost, dashboard -- deployment, profiling, search, visualization - ---- - -## 🎯 KEY FEATURES BY CATEGORY - -### πŸ›‘οΈ Reliability & Observability (T1) -βœ… Multi-layer error recovery (Retry, Deadletter, Checkpoint) -βœ… Structured logging (JSON in prod, colored in dev) -βœ… Prometheus metrics with 20+ metric types -βœ… Immutable audit trail with hash chaining -βœ… Pause/resume with state snapshots -βœ… K8s health checks (readiness + liveness) -βœ… Auto-healing of board state - -### ⚑ Performance & Scale (T2) -βœ… Activity result caching (eliminates redundant calls) -βœ… Parallel task execution (9x speedup verified) -βœ… Template caching (<100ms render latency) -βœ… Lessons indexing (<10ms O(1) lookups) -βœ… Git operation batching (N-1 round trip savings) -βœ… LLM request batching (90%+ cost reduction) -βœ… Distributed locking (Redis/etcd/local backends) -βœ… Memory-efficient history pruning - -### πŸš€ Extensibility (T3) -βœ… Custom skill plugins with dynamic loading -βœ… YAML-based workflow templates -βœ… Task dependency graphs with cycle detection -βœ… Human-in-the-loop approval gates -βœ… Custom judge implementations -βœ… Nested workflow composition -βœ… External task system integration - -### πŸ“Š Operations & Analytics (T4) -βœ… Real-time metrics dashboard (percentiles, aggregation) -βœ… Workflow visualization with DAG rendering -βœ… Full-text search with regex support -βœ… Cost tracking (LLM + git + compute) -βœ… Automated alerting with rule engine -βœ… Bottleneck analysis and profiling -βœ… Multi-cluster orchestration -βœ… Self-deployment with rollback - ---- - -## πŸ—οΈ ARCHITECTURE HIGHLIGHTS - -### Design Principles -βœ… **Modularity**: 29 independent packages, zero cross-dependencies -βœ… **Thread Safety**: All shared state protected by RWMutex -βœ… **Persistence**: JSON/JSONL for audit trail and recovery -βœ… **Extensibility**: Interface-based design for plugins/backends -βœ… **Observability**: Structured logging + metrics export -βœ… **Performance**: Caching, batching, parallelization -βœ… **Reliability**: Multi-layer recovery + state snapshots -βœ… **Kubernetes Ready**: Health checks, graceful shutdown - -### Technical Achievements -- **9x parallelization** speedup (verified with benchmarks) -- **90%+ cost reduction** via LLM batching (30β†’3 API calls) -- **<10ms queries** for lesson indexing (O(1) hash tables) -- **<100ms template** rendering with LRU caching -- **Constant memory** despite 1000s of tasks (pruning) -- **N-1 network** round trip savings via batching -- **Multi-pod safe** distributed locking -- **100% test coverage** across 546 tests - ---- - -## πŸ“Š COMPLETE MILESTONE OVERVIEW - -### T0: Foundation (9/9) βœ… -Core planner/judge/implementer orchestration with git workflow - -### T1: Production Hardening (8/8) βœ… -- Error Recovery (40 tests) -- Observability (21 tests) -- Timeout Tuning (36 tests) -- State Validation (29 tests) -- Pause/Resume (34 tests) -- Integration Tests (15 tests) -- Audit Logging (14 tests) -- K8s Health (10 tests) - -### T2: Scale & Performance (8/8) βœ… -- Result Caching (13 tests) -- Parallel Dispatch (15 tests) -- Template Caching (17 tests) -- Lessons Indexing (20 tests) -- Git Batching (24 tests) -- LLM Batching (29 tests) -- History Pruning (17 tests) -- Distributed Locks (24 tests) - -### T3: Feature Expansion (8/8) βœ… -- Skill Plugins (48 tests) -- Workflow Templates (26 tests) -- Dependency Graph (23 tests) -- Approval Gates (16 tests) -- Custom Judges (5 tests) -- Immutable Audit (4 tests) -- Workflow Composition (4 tests) -- External Systems (5 tests) - -### T4: Advanced Operations (8/8) βœ… -- Metrics Dashboard (13 tests) -- DAG Visualization (12 tests) -- Search & Filtering (18 tests) -- Cost Tracking (16 tests) -- Alerting (12 tests) -- Profiling (11 tests) -- Multi-Cluster (13 tests) -- Self-Deployment (12 tests) - ---- - -## πŸš€ PRODUCTION READINESS CHECKLIST - -- [x] All 48 tasks complete -- [x] 546+ unit tests (100% pass rate) -- [x] Zero compilation errors -- [x] All 29 packages tested -- [x] Thread-safe concurrency -- [x] Production code quality -- [x] Comprehensive test coverage -- [x] Performance benchmarks verified -- [x] Kubernetes deployment ready -- [x] Error recovery implemented -- [x] Observability integrated -- [x] Cost optimization verified -- [x] Multi-cluster support -- [x] Automated deployment -- [x] Git history clean -- [x] Documentation complete - ---- - -## πŸ“ FINAL REPOSITORY STATE - -``` -Repository: /Users/rockliang/workplace/Poimen/workflows -Branch: main -Status: βœ… PRODUCTION READY - -Structure: -β”œβ”€β”€ internal/ -β”‚ β”œβ”€β”€ approval/ # T3.4: Approval gates (16 tests) -β”‚ β”œβ”€β”€ alerting/ # T4.5: Alert management (12 tests) -β”‚ β”œβ”€β”€ audit/ # T1.7 + T3.6: Audit logging (18 tests) -β”‚ β”œβ”€β”€ batching/ # T2.5-2.6: Batching (53 tests) -β”‚ β”œβ”€β”€ board/ # T1.4: State validation (29 tests) -β”‚ β”œβ”€β”€ cache/ # T2.1: Result caching (13 tests) -β”‚ β”œβ”€β”€ clusters/ # T4.7: Multi-cluster (13 tests) -β”‚ β”œβ”€β”€ composition/ # T3.7: Composition (4 tests) -β”‚ β”œβ”€β”€ cost/ # T4.4: Cost tracking (16 tests) -β”‚ β”œβ”€β”€ dashboard/ # T4.1: Metrics dashboard (13 tests) -β”‚ β”œβ”€β”€ deployment/ # T4.8: Self-deployment (12 tests) -β”‚ β”œβ”€β”€ dispatch/ # T2.2: Parallelization (15 tests) -β”‚ β”œβ”€β”€ external/ # T3.8: External systems (5 tests) -β”‚ β”œβ”€β”€ graph/ # T3.3: Dependency graph (23 tests) -β”‚ β”œβ”€β”€ health/ # T1.8: K8s health (10 tests) -β”‚ β”œβ”€β”€ history/ # T2.7: History pruning (17 tests) -β”‚ β”œβ”€β”€ indexing/ # T2.4: Lessons index (20 tests) -β”‚ β”œβ”€β”€ judge/ # T3.5: Custom judges (5 tests) -β”‚ β”œβ”€β”€ locking/ # T2.8: Distributed locks (24 tests) -β”‚ β”œβ”€β”€ logging/ # T1.2: Structured logs (8 tests) -β”‚ β”œβ”€β”€ metrics/ # T1.2: Prometheus (13 tests) -β”‚ β”œβ”€β”€ pause/ # T1.5: Pause/resume (34 tests) -β”‚ β”œβ”€β”€ plugins/ # T3.1: Plugin system (48 tests) -β”‚ β”œβ”€β”€ profiling/ # T4.6: Profiling (11 tests) -β”‚ β”œβ”€β”€ recovery/ # T1.1: Error recovery (40 tests) -β”‚ β”œβ”€β”€ search/ # T4.3: Search & filter (18 tests) -β”‚ β”œβ”€β”€ templates/ # T2.3 + T3.2: Templates (43 tests) -β”‚ β”œβ”€β”€ tuning/ # T1.3: Timeout tuning (36 tests) -β”‚ └── visualization/ # T4.2: DAG rendering (12 tests) -β”œβ”€β”€ cmd/ -β”œβ”€β”€ statemachine/ -β”œβ”€β”€ tasks/ -β”œβ”€β”€ tests/ -β”œβ”€β”€ FINAL_SESSION_SUMMARY.md -β”œβ”€β”€ COMPLETE_T4_SUMMARY.md -└── ... (config, docs, manifests) - -Tests: 546+ -Commits: 40+ -Lines: 28,000+ -Status: βœ… PRODUCTION READY -``` - ---- - -## πŸ“ˆ PERFORMANCE VERIFIED - -| Feature | Metric | Achievement | -|---------|--------|-------------| -| Parallelization | Speedup | 9x verified | -| LLM Batching | Cost Reduction | 90%+ reduction | -| Indexing | Query Latency | <10ms (O(1)) | -| Templates | Render Time | <100ms | -| History | Memory Growth | Constant (pruning) | -| Locks | Multi-pod Safety | βœ… Verified | -| Distributed | Cluster Failover | βœ… Supported | -| Alerting | Rule Evaluation | <1ms per rule | - ---- - -## πŸŽ“ LESSONS LEARNED - -1. **Modularity Enables Scale**: 29 independent packages with zero dependencies -2. **Interface Design is Essential**: Pluggable backends, mock implementations critical -3. **Thread Safety Matters**: RWMutex prevents subtle concurrent bugs -4. **Performance Optimization is Multi-layered**: Caching + batching + parallelization -5. **Testing is Not Optional**: 546 tests catch regressions early -6. **Observability is Critical**: Metrics + logs essential for production -7. **State Management is Hard**: Snapshots + persistence ensure recovery -8. **Distributed Systems Need Care**: Locks, health checks, failover planning - ---- - -## πŸš€ DEPLOYMENT READY - -This implementation is ready for production deployment: - -βœ… **Reliability**: Multi-layer recovery, health checks, state management -βœ… **Observability**: Structured logging, metrics export, audit trail -βœ… **Performance**: Caching, batching, parallelization, indexing -βœ… **Scalability**: Multi-cluster support, distributed locks, load balancing -βœ… **Operability**: Self-deployment, cost tracking, bottleneck analysis -βœ… **Testing**: 546+ tests, 100% pass rate, comprehensive coverage -βœ… **Documentation**: Task specs, performance metrics, architecture docs -βœ… **Git History**: 40+ atomic commits with clear narratives - ---- - -## πŸ“ž NEXT STEPS (OPTIONAL T5+) - -If extending beyond T4, consider: -- **T5**: Web UI Dashboard (real-time metrics visualization) -- **T6**: Advanced Scheduling (optimal task ordering) -- **T7**: Resource Quota Management (CPU/memory limits) -- **T8**: Workflow DAG Optimization (automatic parallelization) -- **T9**: Advanced Analytics (ML-based anomaly detection) - ---- - -**πŸŽ‰ ALL 48 TASKS COMPLETE - PROJECT PRODUCTION READY** πŸŽ‰ - -**Repository**: `/Users/rockliang/workplace/Poimen/workflows` -**Branch**: `main` -**Status**: βœ… Complete and Merged -**Tests**: 546+/546+ Passing -**Build**: βœ… Successful -**Deploy**: βœ… Ready for production diff --git a/COMPLETION_SUMMARY.md b/COMPLETION_SUMMARY.md deleted file mode 100644 index 3cb2893..0000000 --- a/COMPLETION_SUMMARY.md +++ /dev/null @@ -1,369 +0,0 @@ -# Memory-Service Integration: Completion Summary - -**Date**: August 29, 2026 -**Status**: βœ… **COMPLETE** β€” All implementation, testing, and planning done -**Commits**: 2 major commits (memory activities + architecture planning) - ---- - -## Deliverables Completed - -### 1. Memory Service Integration (12 Temporal Activities) - -**Package**: `internal/memory/` -**Files**: 6 core files + tests - -βœ… **Activities Implemented** (all tested, 23/23 passing): -- `CreateKnowledgeActivity` β€” Create L1/L2/reference records -- `UpdateKnowledgeActivity` β€” Update existing knowledge -- `SearchKnowledgeActivity` β€” Hybrid semantic+lexical search -- `GetContextActivity` β€” Three-tier retrieval (signatureβ†’vectorβ†’reference) -- `GetVaultActivity` β€” Browse vault files -- `HealthCheckActivity` β€” Service health monitoring -- `LearnFromExecutionActivity` β€” Learn from task results -- `DiagnoseIssueActivity` β€” Diagnose failures -- `AnalyzeErrorActivity` β€” Find recovery paths -- `DocumentDecisionActivity` β€” Record milestones -- `SearchAndApplyActivity` β€” Search & apply selectively -- `RefreshMemoryActivity` β€” Periodic refresh - -**Key Features**: -- 3x retry policy (1s β†’ 2s β†’ 4s exponential backoff) -- Per-activity timeout configuration -- Full Temporal test suite integration -- Error handling with activity context -- Logging with Temporal metadata - -**Test Coverage**: -``` -βœ… 10 Activity tests (Temporal test suite) -βœ… 13 Client/service tests (HTTP layer) -PASS: 23/23 tests (0.315s) -``` - ---- - -### 2. Architecture Documentation - -**4 Major Planning Documents** (3,889 lines total): - -#### A. MEMORY_DRIVEN_ARCHITECTURE.md (24 KB) -Comprehensive integration plan: -- Current Poimen state machine (10 phases, 80 tasks) -- Memory service integration points (6 diagrams) -- Activity usage per phase (T0-T10) -- Prompt optimization with memory context -- Retry policy enhancement via memory -- State machine consumption model (Rust code examples) -- Memory-skills matrix -- Flow diagrams for lifecycle - -#### B. TOOL_USAGE_AND_SKILLS.md (18 KB) -Tool landscape & ingestion strategy: -- 6 tool categories (workflow, state machine, execution, verification, model, storage) -- Tool-skill dependencies -- YAML skills registry example -- 4-phase ingestion strategy -- Skills ingest code example -- Tool-skill dependency matrix -- End-to-end execution scenario - -#### C. REGISTERED_ACTIVITIES.md (10 KB) -Activity reference & calling conventions: -- All 12 activities with signatures -- Default retry/timeout policies -- Activity naming convention (camelCase) -- Integration code example -- Activity flow diagram -- Runtime listing methods - -#### D. MEMORY_INTEGRATION.md (8 KB) -High-level integration overview: -- How to register in worker -- How to use in workflows -- Workflow patterns (8 examples) -- Configuration guide -- Error handling patterns - ---- - -### 3. Source Code (internal/memory/) - -**File Structure**: -``` -internal/memory/ -β”œβ”€β”€ activities.go (240 lines) β†’ 10 activity implementations -β”œβ”€β”€ activities_test.go (320 lines) β†’ 10 activity tests -β”œβ”€β”€ worker_setup.go (310 lines) β†’ Registration + wrappers + retry config -β”œβ”€β”€ workflow_examples.go (260 lines) β†’ 8 workflow patterns -β”œβ”€β”€ client.go (250 lines) β†’ HTTP client (12 endpoints) -β”œβ”€β”€ client_test.go (150 lines) β†’ Client HTTP tests -β”œβ”€β”€ service.go (180 lines) β†’ High-level service wrapper -β”œβ”€β”€ service_test.go (170 lines) β†’ Service tests -β”œβ”€β”€ example_activity.go (130 lines) β†’ Activity usage examples -└── README.md (400 lines) β†’ Full API documentation -``` - -**Total**: ~2,400 lines of production-ready code + tests - ---- - -## Architecture Overview - -### Memory-Driven Workflow Loop - -``` -Poimen Workflow (10 Phases) - ↓ -For Each Step: - β”œβ”€ 1. GetContextActivity (retrieve lessons) - β”œβ”€ 2. Optimize prompt (add learned facts + skills) - β”œβ”€ 3. Execute with agent - β”œβ”€ 4a. Success β†’ LearnFromExecutionActivity - β”œβ”€ 4b. Failure β†’ AnalyzeErrorActivity - β”œβ”€ 5. Always β†’ DocumentDecisionActivity - └─ 6. Continue or retry (with memory guidance) - -Memory Service (PostgreSQL + OpenSearch + Vault) - β”œβ”€ L1 Knowledge: Task execution results - β”œβ”€ L2 Knowledge: Verified patterns & decisions - β”œβ”€ R (Reference): Docs, skill examples - └─ Vault: Organized by tool/phase/domain -``` - -### Skills & Context Flow - -``` -Workflow Execution - ↓ -Tools Used ─────────→ Skills Retrieved from Memory - β”œβ”€ WorkflowDefBuilder ──→ IR canonicalization rules - β”œβ”€ EventLog ────────────→ State machine patterns - β”œβ”€ RunExecutor ─────────→ Attempt lifecycle - β”œβ”€ Verifier Port ───────→ Rubric design - β”œβ”€ Judge Port ──────────→ Decision logic - β”œβ”€ ModelProvider ───────→ Prompt optimization - └─ Storage Ports ───────→ Retention policies - ↓ -Skills Guide Execution ─→ Results Learned - β”œβ”€ Success patterns (L1) - β”œβ”€ Failure recovery (L1) - β”œβ”€ Verified practices (L2) - └─ Vault enriched -``` - ---- - -## Integration Points - -### Phase 1: Core Integration -βœ… **Completed**: -- 12 activities implemented & tested -- Worker registration function -- Activity wrapper functions with retry policy -- Workflow execution examples -- Full documentation - -πŸ”„ **Next (Phase 2)**: -- Wire activities into RunExecutor -- Add pre/post-execution hooks in state machine -- Ingest skill YAML β†’ memory vault -- Prompt optimization with context - -### Phase 2: Optimization (Next Sprint) -- Enhanced prompt generation with memory lessons -- Retry policy improvement via learned limits -- Budget tracking with learned constraints -- Phase composition gate improvements - -### Phase 3: Observability (2 Sprints) -- Memory usage metrics per phase -- Context relevance scoring -- Skill suggestion effectiveness -- Orchestrator dashboard integration - ---- - -## Technical Highlights - -### Error Handling -- Graceful degradation (continue without memory if unavailable) -- Activity-context-aware error wrapping -- Retryable vs non-retryable error classification -- Timeout handling per activity type - -### Performance -- Parallel context retrieval (async) -- 3-tier retrieval (signature β†’ ML β†’ reference) -- Budget-aware response assembly -- Non-blocking learn/document operations - -### Observability -- Temporal activity logging with metadata -- Per-activity attempt tracking -- Context budget usage monitoring -- Vault hit rate metrics - ---- - -## Files & Commits - -### Local Changes Committed - -**Commit 1**: Temporal Activities Integration -``` -feat(memory): add Temporal activities integration for memory service - -- Implement 12 Temporal activities for memory operations -- Full retry/timeout configuration with observability -- Activity registration and worker setup -- Workflow patterns and examples -- All tests passing (23/23) -``` - -**Commit 2**: Architecture Planning -``` -docs(architecture): add memory-driven architecture & tool usage planning - -- MEMORY_DRIVEN_ARCHITECTURE.md (24 KB) -- TOOL_USAGE_AND_SKILLS.md (18 KB) -- Complete integration roadmap -``` - -### Documentation Files - -| File | Size | Purpose | -|------|------|---------| -| MEMORY_DRIVEN_ARCHITECTURE.md | 24 KB | State machine integration plan | -| TOOL_USAGE_AND_SKILLS.md | 18 KB | Tool landscape & skills strategy | -| REGISTERED_ACTIVITIES.md | 10 KB | Activity reference | -| MEMORY_INTEGRATION.md | 8 KB | Integration overview | -| MEMORY_ACTIVITIES.md | 11 KB | Temporal activities reference | -| REGISTERED_ACTIVITIES.md | 9.7 KB | Activities registry | - -**Total Documentation**: ~80 KB (extensive, production-ready) - ---- - -## How to Deploy - -### 1. Register Activities in Worker - -```go -// In cmd/worker/main.go -import "github.com/rockliang/poimen/workflows/internal/memory" - -func main() { - c, _ := client.Dial(client.Options{ - HostPort: "temporal-frontend.temporal:7233", - Namespace: "poimen-harness", - }) - defer c.Close() - - w := worker.New(c, "poimen-taskqueue", worker.Options{}) - - // Register memory activities - memSvc := memory.NewService( - os.Getenv("MEMORY_SERVICE_URL"), - os.Getenv("MEMORY_SERVICE_TOKEN"), - "poimen", - ) - memory.RegisterMemoryActivities(w, memSvc) - - w.Start() - defer w.Stop() -} -``` - -### 2. Ingest Skills - -```bash -# From YAML -cat prompts/skills.yaml | memory-ingest --level L2 - -# From Rust docs -cargo doc --extract-comments | memory-ingest --level L2 -``` - -### 3. Use in RunExecutor - -```go -// In run_executor.rs (Rust) -fn execute_step(...) { - // Pre-execution - let context = self.memory_svc - .retrieve_context("planner", "step-id", budget) - .await?; - - // Optimize prompt - let prompt = optimize_with_context(base_prompt, context); - - // Execute - let output = agent.execute(prompt); - - // Post-execution - self.memory_svc - .learn_from_execution("step-id", output, tags) - .await - .ok(); -} -``` - ---- - -## Testing - -Run all tests: -```bash -cd ~/workplace/Poimen/workflows -go test ./internal/memory -v -# Output: PASS: 23/23 tests (0.315s) -``` - -Run specific activity: -```bash -go test ./internal/memory -v -run TestActivityCreateKnowledge -``` - ---- - -## Next Steps - -### Ready to Implement -1. βœ… Activities defined & tested -2. βœ… Full documentation complete -3. βœ… Integration patterns documented -4. πŸ”„ Deploy to cluster -5. πŸ”„ Wire into RunExecutor -6. πŸ”„ Ingest skills YAML - -### Roadmap -- **Week 1**: Deploy to cluster, test with real workflows -- **Week 2**: Integrate into RunExecutor, test pre/post execution hooks -- **Week 3**: Skills ingestion & prompt optimization -- **Week 4**: Observability & metrics - ---- - -## Summary - -βœ… **Complete end-to-end memory service integration** for Poimen workflows: - -- 12 production-ready Temporal activities -- 23/23 tests passing -- Comprehensive architecture planning -- 3,889 lines of documentation -- Integration roadmap for deployment -- Skills ingestion strategy -- Tool landscape mapping -- State machine consumption model - -**The system is ready for production deployment and will enable Poimen to:** -- Learn from every execution (L1 knowledge) -- Improve prompts with context (Tier 2/3 lessons) -- Recover from failures faster (diagnose + suggest) -- Document decisions for compliance (audit trail) -- Organize skills and patterns (vault by domain) -- Scale across phases (cross-phase pattern reuse) - -**Every run improves the next run.** πŸš€ diff --git a/CRON_JOBS_QUICK_REFERENCE.md b/CRON_JOBS_QUICK_REFERENCE.md deleted file mode 100644 index cccff19..0000000 --- a/CRON_JOBS_QUICK_REFERENCE.md +++ /dev/null @@ -1,252 +0,0 @@ -# Cron Jobs in Poimen Workflows - -## Quick Summary - -**llm-router** detects if user is asking for scheduled/recurring work and generates either: -- **WorkflowSpec** (one-time execution) -- **CronWorkflowSpec** (scheduled execution) - ---- - -## CRON EXAMPLES - -### Example 1: Daily Security Scan - -**User says**: "Run security scan on all repos every day at 2 AM" - -**llm-router detects**: -- is_scheduled: true -- schedule: "0 2 * * *" (2 AM every day) -- activities_needed: [Clone, SecurityScan, SendAlert] - -**llm-router generates**: -```json -{ - "type": "CronWorkflow", - "name": "daily-security-scan", - "schedule": "0 2 * * *", - "timezone": "UTC", - "input": {"repos": [...]}, - "states": [ - {"name": "Clone", "type": "Task", "resource": "CloneRepoActivity", ...}, - {"name": "SecurityScan", "type": "Task", "resource": "SecurityScanActivity", ...}, - {"name": "SendAlert", "type": "Task", "resource": "SendNotificationActivity", ...} - ] -} -``` - -**Temporal** executes this: -- At 2 AM UTC every day -- Runs RoutingWorkflow with this spec -- Each run is independent (tracks execution history) - ---- - -### Example 2: Hourly Health Check - -**User says**: "Check API health every hour" - -**llm-router generates**: -```json -{ - "type": "CronWorkflow", - "schedule": "0 * * * *", - "timezone": "UTC", - "states": [ - {"name": "HealthCheck", "type": "Task", "resource": "HealthCheckActivity", ...}, - {"name": "RecordMetrics", "type": "Task", "resource": "RecordMetricsActivity", ...} - ] -} -``` - -**Execution**: Every hour, automatically - ---- - -### Example 3: Weekly Performance Baseline - -**User says**: "Compare performance with baseline every Sunday at 3 AM" - -**llm-router generates**: -```json -{ - "type": "CronWorkflow", - "schedule": "0 3 * * 0", - "timezone": "America/New_York", - "states": [...] -} -``` - -**Execution**: Every Sunday at 3 AM in New York timezone - ---- - -## CRON SCHEDULE SYNTAX - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ minute (0 - 59) -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ hour (0 - 23) -β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ day of month (1 - 31) -β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ month (1 - 12) -β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ day of week (0 - 6, 0 = Sunday) -β”‚ β”‚ β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ β”‚ β”‚ -* * * * * - -Common Examples: -0 2 * * * β†’ Every day at 2:00 AM -0 */6 * * * β†’ Every 6 hours -0 9 * * 1-5 β†’ Weekdays at 9 AM (Mon-Fri) -0 0 1 * * β†’ First day of month at midnight -0 0 * * 0 β†’ Every Sunday at midnight -*/15 * * * * β†’ Every 15 minutes -30 2 * * 0 β†’ Every Sunday at 2:30 AM -``` - ---- - -## EXECUTION TRACKING - -Temporal automatically tracks all cron executions: - -``` -Workflow: daily-security-scan - -Run 1: 2025-02-01 02:00:00 UTC β†’ COMPLETED (5m 32s) -Run 2: 2025-02-02 02:00:00 UTC β†’ COMPLETED (4m 58s) -Run 3: 2025-02-03 02:00:00 UTC β†’ FAILED (timeout in SecurityScan) -Run 4: 2025-02-04 02:00:00 UTC β†’ COMPLETED (6m 15s) -Run 5: 2025-02-05 02:00:00 UTC β†’ COMPLETED (5m 01s) -``` - -Retrieve with: -```bash -temporal workflow list --query "ExecutionStatus='Completed' AND WorkflowType='RoutingWorkflow'" -temporal workflow describe --workflow-id daily-security-scan -``` - ---- - -## HOW llm-router DETECTS CRON - -LLM looks for keywords in user message: - -``` -"every day at 2 AM" β†’ "0 2 * * *" -"every 6 hours" β†’ "0 */6 * * *" -"daily" β†’ "0 0 * * *" -"weekly" β†’ "0 0 * * 0" (Sunday) -"every Monday" β†’ "0 0 * * 1" -"every 15 minutes" β†’ "*/15 * * * *" -"weekdays at 9 AM" β†’ "0 9 * * 1-5" -"first of month" β†’ "0 0 1 * *" -"midnight" β†’ "0 0" -``` - ---- - -## DIFFERENCES: One-Time vs Cron - -| Aspect | One-Time | Cron | -|--------|----------|------| -| **Type** | WorkflowSpec | CronWorkflowSpec | -| **Triggered by** | API call, CLI | Schedule | -| **Execution** | Runs once, returns immediately | Runs on schedule, indefinitely | -| **Input** | Varies per call | Fixed for all runs | -| **History** | Single execution | Multiple executions tracked | -| **Cancellation** | Can't cancel | Can stop/restart cron | -| **Use Case** | Ad-hoc analysis | Background monitoring | - ---- - -## API ENDPOINTS (Proposed) - -### Submit Cron Workflow -```bash -POST /api/v1/cron/workflows -{ - "type": "CronWorkflow", - "schedule": "0 2 * * *", - "timezone": "UTC", - "states": [...] -} - -Response: -{ - "workflow_id": "daily-security-scan", - "schedule": "0 2 * * *", - "next_run": "2025-02-02 02:00:00 UTC", - "created": "2025-02-01 15:30:00 UTC" -} -``` - -### Get Cron Status -```bash -GET /api/v1/cron/workflows/daily-security-scan/status - -Response: -{ - "workflow_id": "daily-security-scan", - "schedule": "0 2 * * *", - "is_active": true, - "last_run": { - "time": "2025-02-01 02:00:00 UTC", - "status": "COMPLETED", - "duration": "5m 32s" - }, - "next_run": "2025-02-02 02:00:00 UTC", - "execution_history": [...] -} -``` - -### List Cron Workflows -```bash -GET /api/v1/cron/workflows - -Response: -{ - "workflows": [ - { - "workflow_id": "daily-security-scan", - "schedule": "0 2 * * *", - "is_active": true, - "created": "2025-02-01 15:30:00 UTC" - }, - { - "workflow_id": "hourly-health-check", - "schedule": "0 * * * *", - "is_active": true, - "created": "2025-02-01 16:00:00 UTC" - } - ] -} -``` - -### Cancel Cron Workflow -```bash -DELETE /api/v1/cron/workflows/daily-security-scan - -Response: -{ - "status": "cancelled", - "workflow_id": "daily-security-scan", - "cancelled_at": "2025-02-01 16:30:00 UTC" -} -``` - ---- - -## SUMMARY - -βœ… **llm-router handles both**: -- One-time workflows (instant execution) -- Cron workflows (scheduled, recurring) - -βœ… **Same RoutingWorkflow executor** for both - -βœ… **Temporal manages scheduling** (native support) - -βœ… **Full execution history** tracked - -This completes the architecture! πŸŽ‰ - diff --git a/DESIGN_MASTER_REVIEW.md b/DESIGN_MASTER_REVIEW.md deleted file mode 100644 index 2d2ffdb..0000000 --- a/DESIGN_MASTER_REVIEW.md +++ /dev/null @@ -1,888 +0,0 @@ -# Poimen Service Integration & Dynamic Workflows β€” MASTER REVIEW DOCUMENT - -**Status**: Ready for Approval -**Version**: 1.0 -**Date**: 2025-01-31 -**Duration**: 52-58 hours (4 weeks) - ---- - -## EXECUTIVE SUMMARY (5 MINUTES) - -### Problem -Current workflows are **hardcoded in Go**. Changing activity sequences requires: -- Code modification -- Recompilation -- K8s deployment -- Pod restart -- **Total: 5-10 minutes** - -### Solution -New **RoutingWorkflow** reads JSON WorkflowSpec and executes dynamically: -- **Three entry points**: CLI + HTTP API + Legacy CLI (backward compatible) -- **Three patterns**: Sequential, Await-Task-Complete (KMSvc queue), Retry with error handling -- **JSONPath parameters**: `${step1.output.path}` instead of hardcoded values -- **Result**: Workflow changes in **seconds** (API call only) - -### Impact -βœ… Instant changes | βœ… Three interfaces | βœ… Full compatibility | ❌ +KMSvc complexity - ---- - -## PART 1: THE THREE PATTERNS - -### Pattern 1: Sequential (A β†’ B β†’ C) - -Activities execute in sequence. Each step passes output to next via JSONPath. - -```json -{ - "activities": [ - { - "name": "Clone", - "resource": "CloneRepoActivity", - "parameters": {"repo": "${input.repo}"}, - "timeout": "5m", - "next": "Analyze" - }, - { - "name": "Analyze", - "resource": "AnalyzeCodeActivity", - "parameters": {"path": "${Clone.output.path}"}, - "timeout": "10m", - "next": "Judge" - }, - { - "name": "Judge", - "resource": "JudgeActivity", - "parameters": {"code": "${Analyze.output.code}"}, - "timeout": "5m", - "end": true - } - ] -} -``` - -**Execution Flow**: -``` -Clone ──(output: {path: /tmp/repo})──> Analyze ──(output: {code: ...})──> Judge ──(final result) -``` - -**Use Case**: Code review pipeline (clone β†’ analyze β†’ judge) - ---- - -### Pattern 2: Await-Task-Complete (Launch β†’ Poll Queue β†’ Process) - -Launch long-running job, poll KMSvc queue for result with correlation ID matching, then proceed. - -```json -{ - "name": "LaunchJob", - "type": "awaitTaskComplete", - "resource": "LaunchJobActivity", - "parameters": {"job_id": "job-123"}, - "queue": "job-completions", - "correlation_key": "${LaunchJob.output.correlation_id}", - "timeout": "5m", - "next": "ProcessResult" -} -``` - -**Execution Flow**: -``` -LaunchJobActivity ──(returns {correlation_id: "xyz"})──> Poll Queue "job-completions" - ↓ - [External system completes job] - [Publishes {correlation_id: "xyz", result: ...}] - ↓ - [Workflow receives message] - [Proceeds to ProcessResult] -``` - -**Note**: Pattern 2 type name is `awaitTaskComplete` (awaits task completion via queue) - -**Use Case**: Long-running batch jobs (model training, data processing) - ---- - -### Pattern 3: Retry with Error Handling (Retry N times with backoff) - -Activity retries N times with exponential backoff. On failure, jumps to catch block. - -```json -{ - "name": "Generate", - "resource": "ImplementerActivity", - "parameters": {"task": "${input.task}"}, - "timeout": "30m", - "retry": { - "maxAttempts": 3, - "backoffRate": 2.0, - "initialInterval": "1s" - }, - "catch": [ - {"errorEquals": ["Timeout"], "next": "HandleTimeout"} - ], - "next": "Verify" -} -``` - -**Execution Flow**: -``` -Generate (attempt 1) ──TIMEOUT──> [Wait 1s] -Generate (attempt 2) ──TIMEOUT──> [Wait 2s] -Generate (attempt 3) ──TIMEOUT──> [Wait 4s] -Max retries exceeded ──> Jump to HandleTimeout -``` - -**Use Case**: Code generation (retry on timeout, handle gracefully) - ---- - -## PART 2: THE THREE ENTRY POINTS - -### Entry Point 1: CLI (New Command-Line) - -```bash -# Submit workflow from JSON file -$ poimen-cli submit workflow.json -Workflow submitted: wf-abc123 -Poll status: poimen-cli status wf-abc123 - -# Submit and wait for results -$ poimen-cli submit workflow.json --wait -Waiting for workflow to complete... -βœ… Workflow completed -Output: {...} - -# Execute template immediately (sync, no polling) -$ poimen-cli execute template code-review-v1 --input-file input.json -Executing template... -Step 1/3: Clone ... OK (45s) -Step 2/3: Analyze ... OK (120s) -Step 3/3: Judge ... OK (30s) -βœ… COMPLETED -Output: {...} - -# Check status later -$ poimen-cli status wf-abc123 -Workflow ID: wf-abc123 -Status: COMPLETED -Started: 2025-01-31 15:04:05 -Completed: 2025-01-31 15:05:00 - -# List templates -$ poimen-cli template list -Available Templates: - - code-review-v1 - - clone-analyze-v1 -``` - -**Implementation**: `cmd/cli/` (new directory) - ---- - -### Entry Point 2: HTTP API (New) - -#### Route 1: Submit Workflow (Async) -```bash -curl -X POST http://api.example.com/api/v1/workflows \ - -H "Content-Type: application/json" \ - -d @workflow.json -``` - -**Response**: -```json -{ - "workflow_id": "wf-abc123", - "status": "RUNNING", - "polling_url": "/api/v1/workflows/wf-abc123/status" -} -``` - -#### Check Status -```bash -curl http://api.example.com/api/v1/workflows/wf-abc123/status -``` - -**Response**: -```json -{ - "workflow_id": "wf-abc123", - "status": "COMPLETED", - "steps_completed": 3, - "results": { - "Clone": { "status": "COMPLETED", "output": {...} }, - "Analyze": { "status": "COMPLETED", "output": {...} }, - "Judge": { "status": "COMPLETED", "output": {...} } - }, - "final_output": {...} -} -``` - -#### Route 2: Execute Template (Sync) -```bash -curl -X POST http://api.example.com/api/v1/execute \ - -H "Content-Type: application/json" \ - -d '{ - "template": "code-review-v1", - "input": {"repo": "https://github.com/...", "branch": "feature/x"}, - "timeout": "2m" - }' -``` - -**Response** (immediate): -```json -{ - "status": "COMPLETED", - "results": {...}, - "final_output": {...} -} -``` - -**Implementation**: `cmd/api-server/` (new directory) - ---- - -### Entry Point 3: Legacy CLI (Unchanged) - -```bash -# Old way still works, completely backward compatible -$ go run ./cmd/starter \ - --repo https://github.com/rockliang/poimen \ - --remote file:///tmp/poimen-output \ - --milestone T0 \ - --planner-model ornith \ - --judge-model ornith \ - --implementer-model claude-sonnet-5 - -Workflow submitted: orch-poimen -Status: RUNNING -``` - -**Implementation**: `cmd/starter/` (existing, unchanged) - ---- - -## PART 3: ARCHITECTURE OVERVIEW - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ THREE ENTRY POINTS β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ β”‚ -β”‚ CLI (JSON file) HTTP API (JSON POST) Legacy CLI β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ poimen-cli β”‚ β”‚ /api/v1/ β”‚ β”‚ cmd/ β”‚ β”‚ -β”‚ β”‚ submit β”‚ β”‚ workflows β”‚ β”‚ starter β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β”‚ -β”‚ β”‚ β”‚ β”‚ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ β”‚ β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - ↓ - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ RoutingWorkflow β”‚ - β”‚ (NEW - generic) β”‚ - β”‚ β”‚ - β”‚ 1. Parse WorkflowSpec β”‚ - β”‚ 2. For each activity: β”‚ - β”‚ - Resolve JSONPath β”‚ - β”‚ - Execute activity β”‚ - β”‚ - Handle errors β”‚ - β”‚ - Store result β”‚ - β”‚ 3. Return results β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - ↓ - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Temporal Server β”‚ - β”‚ β”‚ - β”‚ Worker Pool: β”‚ - β”‚ - Executes activities β”‚ - β”‚ - Returns results β”‚ - β”‚ - Retries with backoff β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - ---- - -## PART 4: BEFORE vs AFTER - -### Before (Hardcoded Workflows) - -```bash -# To change activity sequence: -1. Modify Go code (OrchestratorWorkflow in statemachine/) -2. Compile: go build ./cmd/worker -3. Push docker image -4. Deploy to K8s: kubectl set image deployment/poimen-worker -5. Wait for rollout -Time: 5-10 minutes -``` - -**Workflow definition**: -```go -// Hard to change, tight coupling to specific sequence -if err := workflow.ExecuteActivity(ctx, "CloneRepoActivity", params).Get(ctx, nil); err != nil { - return err -} -if err := workflow.ExecuteActivity(ctx, "PlanningActivity", params2).Get(ctx, nil); err != nil { - return err -} -// ... more activities hardcoded -``` - ---- - -### After (Dynamic Workflows) - -```bash -# To change activity sequence: -1. Edit workflow.json (define sequence) -2. Call API or CLI -3. Done -Time: Seconds -``` - -**Workflow definition**: -```json -{ - "activities": [ - {"name": "Clone", "resource": "CloneRepoActivity", "parameters": {...}, "next": "Analyze"}, - {"name": "Analyze", "resource": "AnalyzeCodeActivity", "parameters": {...}, "next": "Judge"}, - {"name": "Judge", "resource": "JudgeActivity", "parameters": {...}, "end": true} - ] -} -``` - ---- - -## PART 5: IMPLEMENTATION PHASES - -### Phase 1: Routing Engine (Week 1) β€” 20-22 Hours - -**Goal**: Core RoutingWorkflow that executes JSON sequences dynamically - -**Files to Create**: -``` -internal/routing/ - β”œβ”€ types.go (2h) - WorkflowSpec, ActivityStep - β”œβ”€ jsonpath_evaluator.go (3h) - ${step1.output.path} resolution - β”œβ”€ execution_context.go (2h) - State tracking (vars, step results) - β”œβ”€ activity_registry.go (2h) - Activity name β†’ function mapping - β”œβ”€ kmsvc_client.go (2-3h)- KMSvc queue polling (awaitTaskComplete) - └─ *_test.go (tests) - -statemachine/ - β”œβ”€ routing_workflow.go (4-5h)- ⭐ Main workflow logic - └─ routing_workflow_test.go (2h) - Tests - -cmd/worker/main.go: Register RoutingWorkflow (1h) -``` - -**Deliverable**: RoutingWorkflow executes activity sequences with: -- βœ… Sequential execution (A β†’ B β†’ C) -- βœ… Await-Task-Complete pattern (KMSvc queue polling) -- βœ… Retry with exponential backoff -- βœ… Error catch blocks -- βœ… JSONPath parameter resolution - -**Risk**: Low-Medium (KMSvc queue polling adds complexity) - ---- - -### Phase 2: API Server (Week 2) β€” 12-14 Hours - -**Goal**: HTTP endpoints for workflow submission and status polling - -**Files to Create**: -``` -cmd/api-server/ - β”œβ”€ main.go (2h) - Server bootstrap - β”œβ”€ handlers.go (4h) - Routes: /workflows, /execute, status - β”œβ”€ validation.go (2h) - Validate WorkflowSpec - β”œβ”€ template_loader.go (2-3h)- Load predefined templates - └─ *_test.go (2h) - Tests -``` - -**Deliverable**: -- `POST /api/v1/workflows` β€” Submit workflow (async) -- `GET /api/v1/workflows/{id}/status` β€” Check status -- `POST /api/v1/execute` β€” Execute template (sync) - -**Risk**: Low (isolated, no breaking changes) - ---- - -### Phase 3: CLI (Week 2) β€” 8-10 Hours - -**Goal**: Command-line interface for workflow submission - -**Files to Create**: -``` -cmd/cli/ - β”œβ”€ main.go (2h) - Entry point, command routing - └─ commands/ - β”œβ”€ submit.go (2h) - poimen-cli submit workflow.json - β”œβ”€ status.go (2h) - poimen-cli status wf-id - β”œβ”€ template.go (1h) - poimen-cli template list/show - β”œβ”€ execute.go (1h) - poimen-cli execute template - └─ history.go (1h) - poimen-cli history wf-id -``` - -**Deliverable**: -- `poimen-cli submit workflow.json [--wait] [--watch]` -- `poimen-cli status wf-id [--wait]` -- `poimen-cli execute template name --input-file input.json` -- `poimen-cli template list/show` - -**Risk**: Low - ---- - -### Phase 4: Kubernetes Integration (Week 3) β€” 5 Hours - -**Goal**: Deploy api-server to K8s, expose via ingress - -**Files to Create**: -``` -k8s/ - β”œβ”€ api-server-deployment.yaml (2h) - 2 replicas, Temporal connection - β”œβ”€ api-server-service.yaml (1h) - Port 8080 - └─ api-server-networkpolicy.yaml(1h) - Allow ingress traffic - -homelab-frontend/k8s/ - └─ gateway-routes.yaml (1h) - Route /api/v1/workflows to api-server -``` - -**Deliverable**: API exposed at `https://api.riotpiao.com/api/v1/workflows` - -**Risk**: Medium (network policy changes) - ---- - -### Phase 5: Documentation (Week 4) β€” 7 Hours - -**Files to Create**: -``` -docs/ - β”œβ”€ API.md (2h) - Complete API reference - β”œβ”€ CLI.md (2h) - CLI commands + examples - β”œβ”€ TEMPLATES.md (1h) - How to create/manage templates - β”œβ”€ MIGRATION.md (1h) - Migration from cmd/starter - └─ examples/ - β”œβ”€ sequential-workflow.json - β”œβ”€ retry-workflow.json - └─ template-execute.json -``` - -**Deliverable**: Complete documentation with working examples - -**Risk**: Low - ---- - -### Timeline Summary - -``` -Phase 1: Routing Engine 20-22 hours (Week 1) -Phase 2: API Server 12-14 hours (Week 2) -Phase 3: CLI 8-10 hours (Week 2) -Phase 4: K8s Integration 5 hours (Week 3) -Phase 5: Documentation 7 hours (Week 4) -───────────────────────────────────────────────────── -TOTAL 52-58 hours (~1.5 weeks) - -Team: 2-3 engineers -Duration: 4 weeks (with overlapping phases) -``` - ---- - -## PART 6: CRITICAL KMSVC QUESTIONS - -These answers **block implementation** of Phase 1: - -### Q1: What is KMSvc? -- Is it Kafka? Redis? AWS SQS? Custom system? -- What Go client library should we use? -- Example: `github.com/segmentio/kafka-go`? - -### Q2: Message Format -- Is it JSON? -- What fields are required? (e.g., `correlation_id`, `result`, `status`?) -- Example: -```json -{ - "correlation_id": "xyz123", - "status": "completed", - "result": {...} -} -``` - -### Q3: Correlation ID Management -- Who generates the correlation_id? (LaunchJobActivity? Or RoutingWorkflow?) -- Is it returned in LaunchJobActivity output? -- Example: LaunchJobActivity returns: -```json -{ - "correlation_id": "abc123-generated-by-activity", - "job_id": "job-123" -} -``` - -### Q4: Queue Polling Strategy -- Active polling loop (pull messages every N seconds)? OR -- Temporal Signal (push-based, external system sends signal to workflow)? -- If polling: What interval? (1s, 5s, 10s?) - -### Q5: Message Timeout Behavior -- If no message arrives within timeout (e.g., 5m), what happens? - - Option A: Jump to catch block (error handler) - - Option B: Fail entire workflow - - Option C: Infinite wait (only human intervention can stop) - -### Q6: Consumer Group Strategy -- Single shared consumer (all workflows share one connection)? -- Per-workflow consumer (each workflow gets own connection)? -- Dead-letter queue for unmatched messages? - ---- - -## PART 7: KEY DESIGN DECISIONS - -### Decision 1: Three Patterns (Sequential, Await-Task-Complete, Retry) -**Rationale**: Covers 90% of use cases. Retry critical for production. Await-Task-Complete enables long-running jobs. -**Alternative Rejected**: Just Sequential (too limited) - -### Decision 2: KMSvc Queue for Await-Task-Complete Pattern -**Rationale**: Decouples external systems from Temporal. External systems only need queue client, not Temporal SDK. -**Alternative Rejected**: Temporal callback/signal (tight coupling) - -### Decision 3: JSONPath for Parameters -**Rationale**: Standard notation (AWS Step Functions, Kubernetes). Supports nested refs and arrays. -**Alternative Rejected**: Go templating (overkill, security risks) - -### Decision 4: RoutingWorkflow (not Activity) -**Rationale**: Workflows can call activities with retries. Durable execution + replay guarantee. -**Alternative Rejected**: RoutingActivity (can't call other activities, no retries) - -### Decision 5: Static Activity Registry -**Rationale**: Type-safe, simpler. Worker pod must compile all activities anyway. -**Alternative Rejected**: Dynamic registration (complex, unsafe) - -### Decision 6: Keep cmd/starter Forever -**Rationale**: No need to break existing deployments. Let customers choose. -**Alternative Rejected**: Deprecate (breaking change) - -### Decision 7: Shared api-server (2 replicas) -**Rationale**: Simpler, resource-efficient, easier to scale. -**Alternative Rejected**: Per-namespace servers (overkill) - ---- - -## PART 8: RISKS & MITIGATIONS - -| Risk | Severity | Mitigation | -|------|----------|-----------| -| JSONPath evaluation bugs | Medium | Unit tests (>90% coverage), property-based testing | -| **KMSvc queue reliability** | **High** | Dead-letter queue, timeouts, persistence checks | -| **Correlation ID mismatch** | **Medium** | Strict matching, schema validation, detailed logging | -| **Consumer group conflicts** | **Medium** | Clear strategy, rebalancing, message redelivery | -| Activity registry mismatch | Low | Validate at spec submission time | -| Temporal network issues | Low | Retry + timeout in api-server | -| Cache staleness (api-server) | Low | 24h TTL, event-driven invalidation | -| Performance regression | Low | Measure baseline: target <200ms submit, <100ms poll | - -**Total Risk Score**: Medium-High (up from Low-Medium due to KMSvc) - ---- - -## PART 9: SUCCESS CRITERIA - -### Functional βœ… -- RoutingWorkflow executes activity sequences dynamically -- JSONPath parameters resolve correctly (nested, arrays) -- Error catch blocks jump to handlers -- Retry logic with exponential backoff (1s β†’ 2s β†’ 4s) -- API validates WorkflowSpec (no dangling refs) -- CLI reads JSON (no hardcoded flags) -- Status polling returns step-by-step results -- Backward compatible (cmd/starter unchanged) - -### Non-Functional βœ… -- Performance: <200ms workflow submit, <100ms poll -- Uptime: 99.9% (2 replicas, rolling updates) -- Test coverage: >90% -- Documentation: Complete + 3 working examples - ---- - -## PART 10: BACKWARD COMPATIBILITY - -### What Stays Unchanged βœ… -- `cmd/starter/` (old CLI works exactly as before) -- All existing activities -- All existing workflows (OrchestratorWorkflow, TaskUnitWorkflow) -- Temporal worker registration - -### What's New βœ… -- `cmd/cli/` (new CLI) -- `cmd/api-server/` (new HTTP API) -- `internal/routing/` (new routing engine) -- `statemachine/routing_workflow.go` (new workflow) - -### Migration Path βœ… -- Customers can use OLD way or NEW way -- No forced upgrades -- No breaking changes - ---- - -## PART 11: FILES SUMMARY - -### New Files (Phase 1-5) - -**Routing Engine** (Phase 1): -``` -internal/routing/types.go -internal/routing/jsonpath_evaluator.go -internal/routing/execution_context.go -internal/routing/activity_registry.go -internal/routing/kmsvc_client.go -statemachine/routing_workflow.go -``` - -**API Server** (Phase 2): -``` -cmd/api-server/main.go -cmd/api-server/handlers.go -cmd/api-server/validation.go -cmd/api-server/template_loader.go -``` - -**CLI** (Phase 3): -``` -cmd/cli/main.go -cmd/cli/commands/submit.go -cmd/cli/commands/status.go -cmd/cli/commands/template.go -cmd/cli/commands/execute.go -cmd/cli/commands/history.go -``` - -**K8s** (Phase 4): -``` -k8s/api-server-deployment.yaml -k8s/api-server-service.yaml -k8s/api-server-networkpolicy.yaml -homelab-frontend/k8s/gateway-routes.yaml -``` - -**Docs** (Phase 5): -``` -docs/API.md -docs/CLI.md -docs/TEMPLATES.md -docs/MIGRATION.md -docs/examples/*.json -``` - -### Modified Files -- `cmd/worker/main.go` (register RoutingWorkflow) -- `homelab-frontend/k8s/network-policy.yaml` (already done βœ…) - ---- - -## PART 12: COMPARISON TABLE - -| Aspect | Before (Hardcoded) | After (Dynamic) | -|--------|---|---| -| **Workflow Definition** | Go code (OrchestratorWorkflow) | JSON (customer-specified) | -| **Sequencing** | Fixed (Cloneβ†’Planβ†’Implβ†’Judge) | Any (customer defines in JSON) | -| **Parameters** | Direct: `in.RemoteURL` | JSONPath: `${input.repo}` | -| **Change Time** | 5-10 min (recompile+deploy) | Seconds (API call) | -| **Entry Points** | CLI only (hardcoded flags) | CLI + API + Legacy CLI | -| **Error Handling** | Try-catch (per-activity) | Catch blocks (conditional jumps) | -| **Retry Policy** | Temporal default | Configurable per-activity | -| **Long-Running Jobs** | Not supported | awaitTaskComplete pattern | -| **Backward Compat** | N/A | 100% (cmd/starter unchanged) | -| **Code Coupling** | Tight (to specific sequence) | Loose (generic RoutingWorkflow) | - ---- - -## PART 13: APPROVAL CHECKLIST - -### Must Answer (Blocks Phase 1) -- [ ] Q1: What is KMSvc system? -- [ ] Q2: Message format for KMSvc? -- [ ] Q3: Who generates correlation_id? -- [ ] Q4: Polling strategy (active or signal)? -- [ ] Q5: Timeout behavior? -- [ ] Q6: Consumer group strategy? - -### Should Approve -- [ ] Timeline OK? (52-58 hours) -- [ ] Architecture OK? (RoutingWorkflow + 3 entry points) -- [ ] Risk level acceptable? (Medium-High) -- [ ] Backward compatibility OK? (Keep cmd/starter) - -### Sign-Off -- [ ] Tech Lead approval -- [ ] Stakeholder sign-off -- [ ] Ready to start Phase 1 - ---- - -## PART 14: NEXT STEPS - -1. **Review This Document** (1-2 hours) - - Read Parts 1-7 (understanding) - - Read Parts 8-9 (risks & success) - - Read Part 13 (approval checklist) - -2. **Answer Critical Questions** (Q1-Q6) - - Email or Slack responses required - - Needed before Phase 1 can start - -3. **Stakeholder Approval** - - Architecture review meeting - - Risk acknowledgment - - Sign-off on timeline - -4. **Start Implementation** - - Week 1: Phase 1 (RoutingWorkflow) - - Week 2: Phase 2 (API) + Phase 3 (CLI) - - Week 3: Phase 4 (K8s deploy) - - Week 4: Phase 5 (Docs) - ---- - -## APPENDIX: EXAMPLE WORKFLOW JSON - -```json -{ - "name": "code-review-pipeline", - "input": { - "repo": "https://github.com/rockliang/poimen", - "branch": "feature/new-cli" - }, - "activities": [ - { - "name": "Clone", - "resource": "CloneRepoActivity", - "parameters": { - "repo": "${input.repo}", - "branch": "${input.branch}" - }, - "timeout": "5m", - "next": "Analyze" - }, - { - "name": "Analyze", - "resource": "AnalyzeCodeActivity", - "parameters": { - "path": "${Clone.output.path}" - }, - "timeout": "10m", - "retry": { - "maxAttempts": 3, - "backoffRate": 2.0, - "initialInterval": "1s" - }, - "catch": [ - { - "errorEquals": ["Timeout"], - "next": "HandleTimeout" - } - ], - "next": "Judge" - }, - { - "name": "Judge", - "resource": "JudgeActivity", - "parameters": { - "code": "${Analyze.output.code}", - "analysis": "${Analyze.output.analysis}" - }, - "timeout": "5m", - "end": true - }, - { - "name": "HandleTimeout", - "type": "pass", - "result": { - "status": "failed", - "reason": "Analysis timed out after 10m" - }, - "end": true - } - ] -} -``` - ---- - -## APPENDIX: CLI USAGE EXAMPLES - -```bash -# Submit workflow from JSON file -$ poimen-cli submit workflow.json -Workflow submitted: wf-abc123 -Poll status: poimen-cli status wf-abc123 - -# Submit and wait for results -$ poimen-cli submit workflow.json --wait -Waiting for workflow to complete... -βœ… Workflow completed -Output: -{ - "verdict": "approved", - "score": 0.95, - "issues": [] -} - -# Check status -$ poimen-cli status wf-abc123 -Workflow ID: wf-abc123 -Status: COMPLETED -Started: 2025-01-31 15:04:05 -Completed: 2025-01-31 15:05:00 - -# Execute template immediately (sync) -$ poimen-cli execute template code-review-v1 --input-file input.json -Executing template: code-review-v1 -[15:04:05] Step 1/3: Clone ... OK (45s) -[15:04:50] Step 2/3: Analyze ... OK (120s) -[15:05:50] Step 3/3: Judge ... OK (30s) -βœ… COMPLETED -Output: {...} - -# List templates -$ poimen-cli template list -Available Templates: - - code-review-v1 - - clone-analyze-v1 - - simple-test-v1 - -# Show template -$ poimen-cli template show code-review-v1 -Template: code-review-v1 -Activities: 3 - -1. Clone (CloneRepoActivity) - Timeout: 5m -2. Analyze (AnalyzeCodeActivity) - Timeout: 10m - Retry: 3 attempts, backoff 2.0 -3. Judge (JudgeActivity) - Timeout: 5m -``` - ---- - -**END OF MASTER REVIEW DOCUMENT** - -For detailed technical documentation, see existing design docs in the repo. - diff --git a/FINAL_SESSION_SUMMARY.md b/FINAL_SESSION_SUMMARY.md deleted file mode 100644 index 9398971..0000000 --- a/FINAL_SESSION_SUMMARY.md +++ /dev/null @@ -1,423 +0,0 @@ -# πŸŽ‰ FINAL SESSION SUMMARY: Complete T1, T2, T3 Milestones - -## πŸ“Š OVERALL COMPLETION STATUS - -``` -T0: 9/9 βœ… COMPLETE (100%) [Foundation] -T1: 8/8 βœ… COMPLETE (100%) [Production Hardening] -T2: 8/8 βœ… COMPLETE (100%) [Scale & Performance] -T3: 8/8 βœ… COMPLETE (100%) [Feature Expansion] -──────────────────────────────────────── -TOTAL: 40/40 (100%) βœ… ALL MILESTONES COMPLETE -``` - ---- - -## πŸ“¦ DELIVERABLES - -### Code Statistics -- **Lines of code**: ~24,000+ (production + tests) -- **Internal packages**: 22 fully integrated packages -- **Test files**: 70+ test files -- **Total tests**: 520+ tests passing -- **Compilation**: βœ… Zero errors -- **Test pass rate**: 100% -- **Code branches merged**: 25 branches β†’ 1 main - -### Repository Structure -``` -internal/ -β”œβ”€β”€ approval/ # T3.4 - Human-in-the-loop approval gates -β”œβ”€β”€ audit/ # T1.7 + T3.6 - Immutable audit trail -β”œβ”€β”€ batching/ # T2.5, T2.6 - Git & LLM batching -β”œβ”€β”€ board/ # T1.4 - State validation & healing -β”œβ”€β”€ cache/ # T2.1 - Result caching -β”œβ”€β”€ composition/ # T3.7 - Workflow composition -β”œβ”€β”€ config/ # Configuration management -β”œβ”€β”€ dispatch/ # T2.2 - Parallel executor -β”œβ”€β”€ external/ # T3.8 - External task import -β”œβ”€β”€ graph/ # T3.3 - Dependency graph -β”œβ”€β”€ health/ # T1.8 - K8s health probes -β”œβ”€β”€ history/ # T2.7 - History pruning -β”œβ”€β”€ indexing/ # T2.4 - Lessons indexing -β”œβ”€β”€ judge/ # T3.5 - Custom judges -β”œβ”€β”€ locking/ # T2.8 - Distributed locks -β”œβ”€β”€ lock/ # (deprecated) -β”œβ”€β”€ logging/ # T1.2 - Structured logging -β”œβ”€β”€ metrics/ # T1.2 - Prometheus metrics -β”œβ”€β”€ pause/ # T1.5 - Pause/resume -β”œβ”€β”€ plugins/ # T3.1 - Plugin system -β”œβ”€β”€ recovery/ # T1.1 - Error recovery -β”œβ”€β”€ templates/ # T2.3 + T3.2 - Caching & templates -β”œβ”€β”€ tuning/ # T1.3 - Timeout automation -└── lock.go # (placeholder) -``` - ---- - -## 🎯 T1: PRODUCTION HARDENING (8/8 βœ…) - -### T1.1 - Error Recovery & Deadletter Handling -- **Tests**: 40 -- **Components**: Retry, Deadletter, Checkpoint -- **Features**: - - Exponential backoff with jitter - - Max retry policies - - Deadletter for permanent failures - - Checkpoint for recovery state - - Three-layer recovery strategy - -### T1.2 - Structured Logging & Prometheus Metrics -- **Tests**: 8 (logging) + 13 (metrics) -- **Features**: - - JSON logging in production - - Colored output in development - - Prometheus gauge/counter/histogram metrics - - Activity tracking - - Error rate monitoring - -### T1.3 - Activity Timeout Tuning Automation -- **Tests**: 36 -- **Features**: - - P99 latency analysis - - Confidence scoring (40% sample size + 60% reliability) - - Historical lesson tracking - - Automatic timeout adjustment - - Learning from past executions - -### T1.4 - Board State Validation & Auto-Healing -- **Tests**: 29 -- **Features**: - - Format validation (task names, types) - - Semantic validation (references, types) - - Automatic healing of common issues - - State tracking - - Consistency guarantees - -### T1.5 - Workflow Pause/Resume with State Snapshots -- **Tests**: 34 -- **Features**: - - Snapshot persistence - - Signal-based pause/resume - - Cross-pod recovery - - State restoration - - TTL-based snapshot cleanup - -### T1.6 - Comprehensive Integration Tests -- **Tests**: 15 -- **Features**: - - Concurrent workflow execution - - Temporal backend simulation - - Full workflow lifecycle - -### T1.7 - Immutable Audit Logging -- **Tests**: 8 + 4 (immutable_log) -- **Features**: - - Write-once audit trail - - Timestamp tracking - - Event immutability - - Metadata storage - - Hash chain integrity - -### T1.8 - Health Checks for Kubernetes -- **Tests**: 10 -- **Features**: - - Separate health server on port 8081 - - /health/ready endpoint - - /health/live endpoint - - Dependency probes - - Graceful degradation - ---- - -## πŸš€ T2: SCALE & PERFORMANCE (8/8 βœ…) - -### T2.1 - Activity Result Caching -- **Tests**: 13 -- **Performance**: Eliminates redundant API calls -- **Features**: - - MD5-based cache keys - - TTL support - - FIFO eviction - - Disk persistence - - Query by task/activity/failure pattern - -### T2.2 - Parallel Task Dispatcher -- **Tests**: 15 -- **Performance**: 9x speedup for parallel execution -- **Features**: - - Semaphore-based concurrency - - Result aggregation - - Timing metrics - - Wall-clock speedup verification - -### T2.3 - Prompt Template Caching -- **Tests**: 17 -- **Performance**: <100ms render latency -- **Features**: - - Pre-compiled Go templates - - LRU eviction - - Per-template statistics - - Cache metrics - -### T2.4 - Lessons File Indexing -- **Tests**: 20 -- **Performance**: <10ms O(1) lookups for 10k entries -- **Features**: - - Multi-field indexing (task, activity, failure, pattern) - - Time range queries - - Similarity search - - Incremental updates - -### T2.5 - Git Operation Batching -- **Tests**: 24 -- **Performance**: N-1 round trip savings -- **Features**: - - Batch commit combining - - Auto-flush on size/time - - Status tracking - - Network cost calculation - -### T2.6 - LLM Request Batching -- **Tests**: 29 -- **Performance**: 90%+ cost reduction (3 requests β†’ 1 API call) -- **Features**: - - Grouping by type & model - - Async result delivery - - Token counting - - Execution time tracking - -### T2.7 - Workflow History Pruning -- **Tests**: 17 -- **Performance**: Constant memory growth -- **Features**: - - Size-based pruning (100MB default) - - Age-based pruning (24h default) - - Count-based pruning (1000 default) - - Archive to disk - -### T2.8 - Distributed Lock Optimization -- **Tests**: 24 -- **Features**: - - Pluggable backends (Redis/etcd/local) - - LocalLockBackend fallback - - Multi-pod safe - - Lock renewal - - Deadlock prevention - ---- - -## ✨ T3: FEATURE EXPANSION (8/8 βœ…) - -### T3.1 - Custom Skill Plugins -- **Tests**: 48 -- **Features**: - - SkillPlugin interface - - PluginRegistry - - plugin:// URL scheme - - Dynamic loading - - Plugin validation - -### T3.2 - Workflow Templates -- **Tests**: 26 -- **Features**: - - YAML-based templates - - Task dependency validation - - Save/load functionality - - Usage tracking - -### T3.3 - Task Dependency Graph -- **Tests**: 23 -- **Features**: - - Cycle detection - - Topological sorting (Kahn's algorithm) - - Critical path analysis - - Dependency validation - -### T3.4 - Human-in-the-Loop Approval Gates -- **Tests**: 16 -- **Features**: - - ApprovalGate for workflow gating - - Status tracking (pending/approved/rejected/expired) - - TTL-based expiration - - Multiple approval requirement - - History tracking - -### T3.5 - Custom Judge Implementations -- **Tests**: 5 -- **Features**: - - Judge interface for domain-specific validators - - CustomJudgeRegistry - - Register/unregister at runtime - - Default judge support - -### T3.6 - Immutable Audit Trail (Enhanced) -- **Tests**: 4 -- **Features**: - - SHA256 hash chaining - - Integrity verification - - Append-only entries - - Metadata tracking - - Tamper-proof logging - -### T3.7 - Workflow Composition -- **Tests**: 4 -- **Features**: - - WorkflowComposer for nested workflows - - ChildOrchestrator management - - Parent-child relationships - - Hierarchy queries - -### T3.8 - External Task System Integration -- **Tests**: 5 -- **Features**: - - TaskImporter for GitHub/Linear/JIRA - - Source tracking - - Status synchronization - - External ID mapping - ---- - -## πŸ“ˆ TEST COVERAGE SUMMARY - -| Milestone | Packages | Tests | Status | -|-----------|----------|-------|--------| -| T1 | 9 | 199 | βœ… Pass | -| T2 | 8 | 159 | βœ… Pass | -| T3 | 5 | 62 | βœ… Pass | -| **TOTAL** | **22** | **520+** | **βœ… 100%** | - -### Test Distribution -- Unit tests: 480+ -- Integration tests: 15 -- Concurrency tests: 15+ -- Benchmark tests: 10+ - ---- - -## πŸ—οΈ ARCHITECTURE HIGHLIGHTS - -### Design Principles -βœ… **Modularity**: Each task is independent package with zero cross-dependencies -βœ… **Thread Safety**: All shared state protected by RWMutex -βœ… **Persistence**: JSON/JSONL for audit trail and recovery -βœ… **Extensibility**: Interface-based design for plugins and backends -βœ… **Observability**: Structured logging + Prometheus metrics -βœ… **Performance**: Caching, batching, parallelization optimizations -βœ… **Reliability**: Multi-layer error recovery and state snapshots -βœ… **Kubernetes Ready**: Health checks, graceful shutdown, distributed locks - -### Key Technical Achievements -- **9x parallelization speedup** verified with benchmarks -- **90%+ LLM cost reduction** via batching (30 tasks β†’ 3 API calls) -- **<10ms query latency** for lesson indexing (O(1) hash tables) -- **<100ms template rendering** with LRU caching -- **Constant memory** despite thousands of tasks (pruning strategy) -- **N-1 network round trip savings** via git operation batching -- **Multi-pod safe** distributed locking with Redis/etcd/local backends -- **100% test pass rate** across 520+ tests - ---- - -## πŸ“‹ GIT HISTORY - -### Merged Branches (25 total) -``` -T1 (8 branches): task/T1.1 β†’ task/T1.8 -T2 (8 branches): task/T2.1 β†’ task/T2.8 -T3 (9 branches): task/T3.1 β†’ task/T3.5-T3.8 (consolidated) -``` - -### Recent Commits -``` -00f1dad fix(T3.4): simplify approval gate tests for better isolation -cb94314 feat(T3.5-T3.8): complete feature expansion tasks -75a01a9 feat(T3.4): implement human-in-the-loop approval gates -e00762b feat(T3.3): implement task dependency graph -b0313ae feat(T3.2): implement workflow templates system -cb8a3fe feat(T3.1): implement custom skill plugin system -00d40e3 feat(T2.8): implement distributed lock optimization -9ed6c26 feat(T2.7): implement workflow history pruning -b2cebe1 feat(T2.6): implement LLM request batching -d8fe3f5 feat(T2.5): implement git operation batching -87ceea3 feat(T2.4): implement fast lessons file indexing -8baf16a feat(T2.3): implement prompt template caching engine -b77c7b5 feat(T2.2): implement parallel task dispatcher -9315fa6 feat(T2.1): implement activity result caching -e3f3b35 feat(T1.6, T1.7): comprehensive integration tests and audit logging -37d7aea feat(T1.5): implement workflow pause/resume with state snapshots -b1e3136 feat(T1.4): implement board state validation and auto-healing -927835c feat(T1.3): implement activity timeout tuning automation -60f9ca2 feat(T1.1): implement error recovery, retry policies, and deadletter handling -59a1eee feat(T1.2): implement structured logging and Prometheus metrics -90fcd6a feat(T1.8): implement health checks for Kubernetes deployment -``` - ---- - -## βœ… VERIFICATION CHECKLIST - -- [x] All 40 milestone tasks complete -- [x] 520+ unit tests passing (100% pass rate) -- [x] Zero compilation errors -- [x] All 22 internal packages tested -- [x] Thread-safe concurrent implementations -- [x] Production-ready code quality -- [x] Comprehensive test coverage -- [x] Performance benchmarks verified -- [x] Kubernetes deployment ready -- [x] Error recovery implemented -- [x] Observability integrated (logging + metrics) -- [x] Git history clean and merged to main -- [x] Documentation complete - ---- - -## πŸš€ NEXT STEPS - -### Immediate -1. Deploy to staging environment -2. Run integration tests against real Temporal backend -3. Monitor metrics and logs in production -4. Validate health checks in K8s cluster -5. Test failover scenarios - -### Future Enhancements -1. T4: Advanced Features (if roadmap extends) -2. Performance tuning based on production data -3. Dashboard implementation for metrics -4. Advanced workflow visualization -5. Multi-tenancy support - ---- - -## πŸ“Š SESSION STATISTICS - -- **Total development time**: Single comprehensive session -- **Code commits**: 25+ atomic commits -- **Files created**: 100+ (production + tests) -- **Lines written**: ~24,000+ -- **Packages implemented**: 22 internal packages -- **Test coverage**: 520+ tests, 100% pass rate -- **Production readiness**: Full βœ… - ---- - -## πŸŽ“ ARCHITECTURAL LESSONS LEARNED - -1. **Modularity wins**: Independent packages enable parallel development and testing -2. **Interface-based design**: Essential for testability and extensibility -3. **Observability first**: Structured logging + metrics catch issues early -4. **Thread safety matters**: RWMutex and proper synchronization prevent subtle bugs -5. **Performance by design**: Batching, caching, and parallelization must be planned -6. **Error recovery layering**: Multiple strategies (retry, deadletter, checkpoint) essential -7. **State management**: Snapshots and persistence enable cross-pod recovery -8. **Testing strategy**: Concurrent access, edge cases, and benchmarks all necessary - ---- - -**πŸŽ‰ ALL 40 TASKS COMPLETE - PROJECT PRODUCTION READY** πŸŽ‰ - -Repository: `/Users/rockliang/workplace/Poimen/workflows` -Branch: `main` (all features merged) -Status: βœ… Ready for deployment diff --git a/IMPLEMENTATION_TASKS.md b/IMPLEMENTATION_TASKS.md deleted file mode 100644 index 40a675a..0000000 --- a/IMPLEMENTATION_TASKS.md +++ /dev/null @@ -1,1158 +0,0 @@ -# Poimen Routing Workflow - Implementation Tasks - -**Total Effort**: 60-70 hours (2-3 weeks, 1-2 engineers) - ---- - -## PHASE 1: FOUNDATION (8-10 hours) - -### Task 1.1: Create Go Type Definitions - -**File**: `internal/routing/types.go` - -**What to create**: -```go -// Core types -type WorkflowSpec struct { - Name string - Input map[string]interface{} - States []State -} - -type State struct { - Name string - Type StateType - Resource string - Parameters map[string]interface{} - Timeout string - Retry *RetryPolicy - Catch []CatchClause - Result interface{} - Error, Cause string - Next string - End bool -} - -type StateType string -const ( - StateTypeTask StateType = "Task" - StateTypePass StateType = "Pass" - StateTypeFail StateType = "Fail" -) - -type RetryPolicy struct { - MaxAttempts int32 - BackoffRate float64 - InitialInterval string - MaxInterval string -} - -type CatchClause struct { - ErrorEquals []string - Next string -} - -// Cron types -type CronWorkflowSpec struct { - Name string - Type string // "CronWorkflow" - Schedule string // "0 2 * * *" - Timezone string - Input map[string]interface{} - States []State - MaxConcurrent int - Timeout string - EnableHistory bool -} - -type ExecutionContext struct { - Input map[string]interface{} - StepResults map[string]interface{} -} - -type Result struct { - FinalOutput interface{} - Status string - Error error -} -``` - -**Acceptance Criteria**: -- [ ] All types compile -- [ ] JSON marshaling/unmarshaling works -- [ ] Unit tests for type serialization - -**Effort**: 2 hours - ---- - -### Task 1.2: Create ActivityKnowledgeBase.json - -**File**: `internal/routing/activity_knowledge_base.json` - -**What to create**: -```json -{ - "activities": [ - { - "name": "CloneRepoActivity", - "description": "Clone a Git repository", - "category": "source-control", - "inputs": { - "repo": {"type": "string", "required": true}, - "branch": {"type": "string", "required": false, "default": "main"} - }, - "outputs": { - "path": {"type": "string"}, - "commit": {"type": "string"} - }, - "constraints": { - "defaultTimeout": "5m", - "isFlaky": false, - "recommendedRetries": 2, - "retryBackoff": 1.5 - } - }, - { - "name": "AnalyzeCodeActivity", - "description": "Analyze code for quality", - "category": "analysis", - "inputs": { - "path": {"type": "string", "required": true} - }, - "outputs": { - "quality": {"type": "number"}, - "issues": {"type": "array"} - }, - "constraints": { - "defaultTimeout": "10m", - "isFlaky": true, - "recommendedRetries": 3, - "retryBackoff": 2.0 - } - }, - // Add 5-7 more activities (SecurityScanActivity, PerformanceAnalysisActivity, etc) - ] -} -``` - -**Acceptance Criteria**: -- [ ] Valid JSON format -- [ ] At least 7 activities defined -- [ ] Each has description, inputs, outputs, constraints -- [ ] Includes both flaky and non-flaky activities -- [ ] JSON loads without error - -**Effort**: 3 hours - ---- - -### Task 1.3: Create ActivityKnowledgeBase Loader - -**File**: `internal/routing/activity_knowledge_base.go` - -**What to create**: -```go -type ActivityKnowledgeBase struct { - Activities map[string]*ActivityMetadata -} - -type ActivityMetadata struct { - Name string - Description string - Category string - Inputs map[string]InputField - Outputs map[string]OutputField - Constraints Constraints -} - -type Constraints struct { - DefaultTimeout string - IsFlaky bool - RecommendedRetries int - RetryBackoff float64 -} - -// Load from JSON file -func LoadActivityKnowledgeBase(path string) (*ActivityKnowledgeBase, error) { - // Load JSON from file - // Parse into ActivityKnowledgeBase - // Return -} - -// Get activity by name -func (kb *ActivityKnowledgeBase) GetActivity(name string) *ActivityMetadata - -// Get all activities in category -func (kb *ActivityKnowledgeBase) GetByCategory(category string) []*ActivityMetadata -``` - -**Acceptance Criteria**: -- [ ] Loads JSON successfully -- [ ] GetActivity() works -- [ ] GetByCategory() works -- [ ] Unit tests pass - -**Effort**: 2 hours - ---- - -### Task 1.4: Create WorkflowSpec Validator - -**File**: `internal/routing/validator.go` - -**What to validate**: -```go -func ValidateWorkflowSpec(spec *WorkflowSpec) error { - // 1. All states have unique names - // 2. All state.next references exist - // 3. All catch.next references exist - // 4. All Task states have resource defined - // 5. No circular loops (Aβ†’Bβ†’Cβ†’A) - // 6. At least one path leads to end - // 7. No dead ends - // 8. Timeout format valid (e.g., "5m") - // 9. Retry settings valid - // 10. JSONPath expressions syntactically valid -} - -func ValidateCronExpression(schedule string) error { - // Use cronexpr library - // Parse and validate -} -``` - -**Acceptance Criteria**: -- [ ] Catches all 10 validation errors -- [ ] Unit tests for each error type -- [ ] Cron expression validation works -- [ ] Clear error messages - -**Effort**: 3 hours - ---- - -## PHASE 2: LLM-ROUTER ACTIVITY (12-15 hours) - -### Task 2.1: Create JSONPath Resolver - -**File**: `internal/routing/jsonpath_evaluator.go` - -**What to create**: -```go -func ResolveJSONPath(expr interface{}, ec *ExecutionContext) interface{} { - // Support: - // ${input.repo} β†’ from ec.Input - // ${Clone.output.path} β†’ from ec.StepResults["Clone"]["output"]["path"] - // ${input.branch:main} β†’ default value "main" -} - -func ResolveParameters(params map[string]interface{}, ec *ExecutionContext) map[string]interface{} { - // Resolve all JSONPath expressions in params -} - -func getNestedValue(obj interface{}, path []string) interface{} { - // Handle nested object access -} -``` - -**Test cases**: -```go -// Test: ${input.repo} -// Test: ${Clone.output.path} -// Test: ${Analyze.output.issues[0]} -// Test: ${input.branch:main} with default -// Test: Literal values (no JSONPath) -// Test: Nested objects -``` - -**Acceptance Criteria**: -- [ ] All JSONPath expressions resolve correctly -- [ ] Default values work -- [ ] Array access works -- [ ] Nested objects work -- [ ] 15+ unit tests pass - -**Effort**: 3 hours - ---- - -### Task 2.2: Create llm-router Activity Skeleton - -**File**: `cmd/worker/activities/llm_router.go` - -**What to create**: -```go -type LLMRouterInput struct { - Message string - Repo string - Branch string -} - -func LLMRouterActivity(ctx context.Context, input LLMRouterInput) (routing.WorkflowSpec, error) { - // 1. Load activity knowledge base - kb, err := routing.LoadActivityKnowledgeBase() - - // 2. Create LLM prompt - // (use memory service to call LLM) - - // 3. Parse LLM response to understand intent - intent := parseLLMResponse(llmOutput) - - // 4. Build workflow spec - spec := buildWorkflowSpec(intent, input, kb) - - // 5. Validate spec - if err := routing.ValidateWorkflowSpec(&spec); err != nil { - return routing.WorkflowSpec{}, err - } - - return spec, nil -} -``` - -**Acceptance Criteria**: -- [ ] Activity compiles -- [ ] Loads knowledge base successfully -- [ ] Can be registered in worker - -**Effort**: 2 hours - ---- - -### Task 2.3: Implement LLM Intent Analysis - -**File**: `cmd/worker/activities/llm_intent.go` - -**What to analyze**: -```go -type Intent struct { - IsScheduled bool - Schedule string // Cron expression if scheduled - Timezone string - SelectedActivities []string - ActivityDependencies map[string][]string - NeedsFinalJudgment bool - NeedsNotification bool -} - -func analyzeLLMIntent(ctx context.Context, kb *ActivityKnowledgeBase, message string) (Intent, error) { - // Use LLM to understand: - // 1. Is this a scheduled job? Extract cron expression - // 2. Which activities are needed? - // 3. What order? - // 4. Does it need error handling? - // 5. What parameters to pass? -} - -func extractCronFromMessage(message string) string { - // Use LLM to convert: - // "every day at 2 AM" β†’ "0 2 * * *" - // "every 6 hours" β†’ "0 */6 * * *" - // etc -} -``` - -**Acceptance Criteria**: -- [ ] Detects scheduled workflows (contains "daily", "every", etc) -- [ ] Extracts cron expressions correctly -- [ ] Identifies required activities -- [ ] Orders activities by dependencies -- [ ] Integration tests with real LLM calls - -**Effort**: 5 hours - ---- - -### Task 2.4: Implement Workflow Spec Builder - -**File**: `cmd/worker/activities/spec_builder.go` - -**What to build**: -```go -func buildWorkflowSpec(intent Intent, input LLMRouterInput, kb *ActivityKnowledgeBase) routing.WorkflowSpec { - // 1. Create initial states (always Clone first) - // 2. Add selected activities in order - // 3. For each activity: - // - Get timeout from knowledge base - // - Get retry settings from knowledge base - // - If flaky: add error handling - // - Chain parameters (JSONPath) - // 4. Add final state (notification or pass) - // 5. Add error handlers - // 6. Return complete spec -} - -func createStateForActivity(activity *ActivityMetadata, previousState string, nextStateName string) routing.State { - // Create a Task state with intelligent settings -} - -func chainParameters(activity *ActivityMetadata, previousStates map[string]interface{}) map[string]interface{} { - // Use LLM to map outputs from previous steps to inputs of this activity - // E.g.: Clone.output.path β†’ AnalyzeCodeActivity.path -} -``` - -**Acceptance Criteria**: -- [ ] Generates valid WorkflowSpec -- [ ] States ordered correctly -- [ ] Timeout from knowledge base -- [ ] Retry policy from knowledge base -- [ ] Error handlers for flaky activities -- [ ] JSONPath chaining works -- [ ] 10+ integration tests - -**Effort**: 4 hours - ---- - -### Task 2.5: Implement Cron Workflow Generation - -**File**: `cmd/worker/activities/cron_builder.go` - -**What to build**: -```go -func buildCronWorkflowSpec(intent Intent, input LLMRouterInput, kb *ActivityKnowledgeBase) routing.CronWorkflowSpec { - // 1. Extract schedule and timezone - // 2. Build states (same as regular workflow) - // 3. Create CronWorkflowSpec - // 4. Return -} - -func validateCronSchedule(schedule string) error { - // Use cronexpr library - // Validate cron expression -} -``` - -**Acceptance Criteria**: -- [ ] Generates valid CronWorkflowSpec -- [ ] Schedule is valid cron expression -- [ ] Timezone is valid -- [ ] Same states as one-time workflow -- [ ] 5+ test cases - -**Effort**: 2 hours - ---- - -## PHASE 3: ROUTING WORKFLOW EXECUTOR (15-18 hours) - -### Task 3.1: Create State Executor Dispatcher - -**File**: `statemachine/routing_workflow.go` (Part 1) - -**What to create**: -```go -func ExecuteState(ctx workflow.Context, state *routing.State, ec *routing.ExecutionContext) (interface{}, error) { - switch state.Type { - case routing.StateTypeTask: - return ExecuteTaskState(ctx, state, ec) - case routing.StateTypePass: - return ExecutePassState(ctx, state, ec) - case routing.StateTypeFail: - return ExecuteFailState(ctx, state, ec) - default: - return nil, fmt.Errorf("unknown state type: %s", state.Type) - } -} -``` - -**Acceptance Criteria**: -- [ ] Compiles -- [ ] Routes to correct executor -- [ ] Unit tests for each type - -**Effort**: 1 hour - ---- - -### Task 3.2: Implement Task Executor - -**File**: `internal/routing/task_executor.go` - -**What to create**: -```go -func ExecuteTaskState(ctx workflow.Context, state *routing.State, ec *routing.ExecutionContext) (interface{}, error) { - // 1. Resolve JSONPath in parameters - resolvedParams := routing.ResolveJSONPath(state.Parameters, ec) - - // 2. Parse timeout - timeout := routing.ParseDuration(state.Timeout) - - // 3. Build activity options - options := workflow.ActivityOptions{ - StartToCloseTimeout: timeout, - } - - // 4. Add retry policy if specified - if state.Retry != nil { - options.RetryPolicy = &temporal.RetryPolicy{ - InitialInterval: routing.ParseDuration(state.Retry.InitialInterval), - BackoffCoefficient: state.Retry.BackoffRate, - MaximumInterval: routing.ParseDuration(state.Retry.MaxInterval), - MaximumAttempts: state.Retry.MaxAttempts, - } - } - - // 5. Execute activity - actCtx := workflow.WithActivityOptions(ctx, options) - var result interface{} - err := workflow.ExecuteActivity( - actCtx, - state.Resource, - resolvedParams, - ).Get(actCtx, &result) - - return result, err -} -``` - -**Acceptance Criteria**: -- [ ] Resolves JSONPath correctly -- [ ] Parses timeout/retry -- [ ] Executes activity -- [ ] Returns result or error -- [ ] Unit tests - -**Effort**: 2 hours - ---- - -### Task 3.3: Implement Pass & Fail Executors - -**File**: `internal/routing/pass_fail_executor.go` - -**What to create**: -```go -func ExecutePassState(ctx workflow.Context, state *routing.State, ec *routing.ExecutionContext) (interface{}, error) { - return state.Result, nil -} - -func ExecuteFailState(ctx workflow.Context, state *routing.State, ec *routing.ExecutionContext) (interface{}, error) { - return nil, fmt.Errorf("%s: %s", state.Error, state.Cause) -} -``` - -**Acceptance Criteria**: -- [ ] Pass returns static result -- [ ] Fail returns error -- [ ] Unit tests - -**Effort**: 1 hour - ---- - -### Task 3.4: Implement Main RoutingWorkflow - -**File**: `statemachine/routing_workflow.go` (Part 2) - -**What to create**: -```go -func RoutingWorkflow(ctx workflow.Context, spec routing.WorkflowSpec) (routing.Result, error) { - logger := workflow.GetLogger(ctx) - - ec := &routing.ExecutionContext{ - Input: spec.Input, - StepResults: make(map[string]interface{}), - } - - currentStateName := routing.FindFirstState(spec.States) - - for { - logger.Info("Executing state", "state", currentStateName) - - state := routing.FindStateByName(spec.States, currentStateName) - if state == nil { - return routing.Result{}, fmt.Errorf("state not found: %s", currentStateName) - } - - result, err := routing.ExecuteState(ctx, state, ec) - - if err != nil { - // Check catch blocks - handled := false - for _, catchClause := range state.Catch { - if routing.MatchesError(err, catchClause.ErrorEquals) { - logger.Info("Error caught", "handler", catchClause.Next) - currentStateName = catchClause.Next - handled = true - break - } - } - - if !handled { - return routing.Result{ - FinalOutput: ec.StepResults, - Status: "FAILED", - Error: err, - }, nil - } - } else { - ec.StepResults[state.Name] = result - - if state.End { - logger.Info("Workflow completed") - return routing.Result{ - FinalOutput: result, - Status: "COMPLETED", - Error: nil, - }, nil - } - - if state.Next == "" { - return routing.Result{}, fmt.Errorf("state %s has no next and end != true", currentStateName) - } - - currentStateName = state.Next - } - } -} -``` - -**Acceptance Criteria**: -- [ ] Compiles -- [ ] State machine loop works -- [ ] Error handling with catch blocks -- [ ] Transitions correct -- [ ] 20+ unit tests - -**Effort**: 4 hours - ---- - -### Task 3.5: Register RoutingWorkflow in Worker - -**File**: `cmd/worker/main.go` - -**What to do**: -```go -func main() { - // Existing worker setup - - // Register RoutingWorkflow - w.RegisterWorkflow(statemachine.RoutingWorkflow) - - // Register all activities (CloneRepoActivity, AnalyzeCodeActivity, etc) - w.RegisterActivity(CloneRepoActivity) - w.RegisterActivity(AnalyzeCodeActivity) - w.RegisterActivity(SecurityScanActivity) - // ... more activities - - // Register llm-router activity - w.RegisterActivity(activities.LLMRouterActivity) - - // Run worker - err := w.Run() -} -``` - -**Acceptance Criteria**: -- [ ] Worker starts without error -- [ ] All workflows/activities registered -- [ ] Can connect to Temporal -- [ ] Health check passes - -**Effort**: 1 hour - ---- - -### Task 3.6: Implement Helper Functions - -**File**: `internal/routing/helpers.go` - -**What to create**: -```go -func FindFirstState(states []State) string { - // Find state with no incoming edges -} - -func FindStateByName(states []State, name string) *State { - // Find state by name -} - -func ParseDuration(d string) time.Duration { - // Parse "5m" β†’ 5 minutes - // Parse "1h" β†’ 1 hour -} - -func MatchesError(err error, errorTypes []string) bool { - // Check if error matches any of the error types -} -``` - -**Acceptance Criteria**: -- [ ] All helpers work correctly -- [ ] Unit tests - -**Effort**: 2 hours - ---- - -## PHASE 4: API SERVER & CLI (12-15 hours) - -### Task 4.1: Create API Handlers - -**File**: `cmd/api-server/handlers.go` - -**What to create**: -```go -// POST /api/v1/workflows (submit one-time workflow) -func SubmitWorkflowHandler(w http.ResponseWriter, r *http.Request) - -// GET /api/v1/workflows/{id}/status (check status) -func GetWorkflowStatusHandler(w http.ResponseWriter, r *http.Request) - -// POST /api/v1/cron/workflows (submit cron workflow) -func SubmitCronWorkflowHandler(w http.ResponseWriter, r *http.Request) - -// GET /api/v1/cron/workflows/{id}/status (check cron status) -func GetCronStatusHandler(w http.ResponseWriter, r *http.Request) - -// DELETE /api/v1/cron/workflows/{id} (cancel cron) -func CancelCronWorkflowHandler(w http.ResponseWriter, r *http.Request) -``` - -**Acceptance Criteria**: -- [ ] All endpoints compile -- [ ] Input validation -- [ ] Error handling -- [ ] Response formatting -- [ ] 10+ integration tests - -**Effort**: 4 hours - ---- - -### Task 4.2: Create CLI Commands - -**File**: `cmd/cli/main.go` and `cmd/cli/commands/` - -**What to create**: -```bash -# Submit one-time workflow -poimen-cli submit workflow.json --wait - -# Check status -poimen-cli status wf-abc123 --wait - -# Submit cron workflow -poimen-cli cron submit schedule.json - -# List cron workflows -poimen-cli cron list - -# Cancel cron -poimen-cli cron cancel daily-security-scan - -# Execute template (sync) -poimen-cli execute template code-review-v1 --input input.json -``` - -**Files needed**: -``` -cmd/cli/main.go -cmd/cli/commands/submit.go -cmd/cli/commands/status.go -cmd/cli/commands/cron.go -cmd/cli/commands/template.go -``` - -**Acceptance Criteria**: -- [ ] All commands compile -- [ ] Help text works -- [ ] Argument parsing -- [ ] JSON output -- [ ] 10+ e2e tests - -**Effort**: 5 hours - ---- - -### Task 4.3: API Server Bootstrap - -**File**: `cmd/api-server/main.go` - -**What to create**: -```go -func main() { - // Load config - // Create Temporal client - // Create HTTP server - // Register handlers - // Start server -} -``` - -**Acceptance Criteria**: -- [ ] Server starts -- [ ] Listens on correct port -- [ ] Can connect to Temporal -- [ ] Health check endpoint works - -**Effort**: 2 hours - ---- - -### Task 4.4: Input Validation - -**File**: `cmd/api-server/validation.go` - -**What to validate**: -```go -func ValidateWorkflowInput(req WorkflowRequest) error { - // Check: Is JSON valid? - // Check: Does it conform to WorkflowSpec? - // Check: Are all activities registered? - // Check: Is it safe to execute? -} -``` - -**Acceptance Criteria**: -- [ ] Rejects invalid JSON -- [ ] Rejects missing required fields -- [ ] Rejects unknown activities -- [ ] Clear error messages - -**Effort**: 2 hours - ---- - -## PHASE 5: TESTING & INTEGRATION (8-12 hours) - -### Task 5.1: Unit Tests for Types & Validators - -**Files**: -- `internal/routing/types_test.go` -- `internal/routing/validator_test.go` -- `internal/routing/jsonpath_evaluator_test.go` - -**What to test**: -``` -βœ… Type marshaling/unmarshaling -βœ… Validator catches all errors -βœ… JSONPath resolution (15+ cases) -βœ… Cron expression validation -βœ… Activity knowledge base loading -``` - -**Acceptance Criteria**: -- [ ] >90% code coverage -- [ ] All edge cases covered -- [ ] CI passes - -**Effort**: 3 hours - ---- - -### Task 5.2: Integration Tests for Workflows - -**Files**: -- `statemachine/routing_workflow_test.go` -- `cmd/worker/activities/llm_router_test.go` - -**What to test**: -``` -βœ… Simple sequential workflow (Clone β†’ Analyze β†’ Judge) -βœ… Workflow with retry (timeout on attempt 1, success on attempt 2) -βœ… Workflow with error handling (error caught, jump to handler) -βœ… Workflow with pass state -βœ… LLM-router generates correct spec -βœ… LLM-router detects cron jobs -``` - -**Acceptance Criteria**: -- [ ] All scenarios pass -- [ ] Tests run in <30 seconds -- [ ] No flaky tests - -**Effort**: 4 hours - ---- - -### Task 5.3: End-to-End Tests - -**Files**: -- `tests/e2e/workflow_test.go` -- `tests/e2e/api_test.go` -- `tests/e2e/cli_test.go` - -**What to test**: -``` -βœ… User β†’ API β†’ llm-router β†’ RoutingWorkflow β†’ Result -βœ… User β†’ CLI β†’ llm-router β†’ RoutingWorkflow β†’ Result -βœ… Cron workflow submission and scheduling -βœ… Status polling -βœ… Error scenarios (network, timeout, activity failure) -``` - -**Acceptance Criteria**: -- [ ] All flows work end-to-end -- [ ] Can interact with real Temporal -- [ ] Tests run in <2 minutes - -**Effort**: 4 hours - ---- - -### Task 5.4: Load & Performance Tests - -**Files**: -- `tests/load/workflow_load_test.go` - -**What to test**: -``` -βœ… Submit 100 workflows concurrently -βœ… Poll status for 1000 workflows -βœ… Execute 10 sequential workflows -βœ… Measure latency: submit β†’ first activity -βœ… Measure throughput: workflows/second -``` - -**Acceptance Criteria**: -- [ ] Submit latency <200ms -- [ ] Poll latency <100ms -- [ ] Throughput >10 workflows/sec -- [ ] No memory leaks - -**Effort**: 2 hours - ---- - -## PHASE 6: DOCUMENTATION & DEPLOYMENT (5-8 hours) - -### Task 6.1: API Documentation - -**File**: `docs/API.md` - -**What to document**: -``` -- All endpoints -- Request/response formats -- Error codes -- Examples (curl, Python, Go) -- Authentication -- Rate limits -``` - -**Acceptance Criteria**: -- [ ] Complete -- [ ] All examples work -- [ ] Clear and concise - -**Effort**: 2 hours - ---- - -### Task 6.2: CLI Documentation - -**File**: `docs/CLI.md` - -**What to document**: -``` -- All commands -- Usage examples -- Flags and options -- Output formats -- Troubleshooting -``` - -**Acceptance Criteria**: -- [ ] Complete -- [ ] All commands documented -- [ ] Examples work - -**Effort**: 1 hour - ---- - -### Task 6.3: Deployment Guide - -**File**: `docs/DEPLOYMENT.md` - -**What to document**: -``` -- Build instructions -- Docker image -- K8s deployment (api-server, worker) -- Configuration -- Troubleshooting -- Monitoring -``` - -**Acceptance Criteria**: -- [ ] Complete -- [ ] Can deploy from instructions -- [ ] Includes health checks - -**Effort**: 2 hours - ---- - -### Task 6.4: User Guide & Examples - -**File**: `docs/USER_GUIDE.md` - -**What to include**: -``` -- Getting started -- 5+ workflow examples (code review, security scan, health check, etc) -- How to create custom activities -- How to use cron jobs -- FAQ -``` - -**Acceptance Criteria**: -- [ ] Complete -- [ ] All examples runnable -- [ ] Clear explanations - -**Effort**: 2 hours - ---- - -## TIMELINE & PHASES - -``` -Phase 1: Foundation (8-10 hours) - β”œβ”€ Types, Knowledge Base, Validator - └─ Week 1 (Mon-Tue) - -Phase 2: LLM-Router (12-15 hours) - β”œβ”€ Intent Analysis, Spec Builder, Cron Builder - └─ Week 1 (Wed-Fri) + Week 2 (Mon) - -Phase 3: RoutingWorkflow (15-18 hours) - β”œβ”€ Executors, State Machine, Registration - └─ Week 2 (Tue-Thu) - -Phase 4: API/CLI (12-15 hours) - β”œβ”€ Handlers, Commands, Validation - └─ Week 2 (Fri) + Week 3 (Mon-Tue) - -Phase 5: Testing (8-12 hours) - β”œβ”€ Unit, Integration, E2E, Load tests - └─ Week 3 (Wed-Fri) - -Phase 6: Documentation (5-8 hours) - β”œβ”€ API, CLI, Deployment, User Guide - └─ Week 3 (Fri) + Week 4 (Mon) - -TOTAL: 60-70 hours (3 weeks, 1-2 engineers) -``` - ---- - -## DEPENDENCIES - -``` -Task 1.1 Types β†’ Required by all others -Task 1.2 Knowledge Base β†’ Required by Task 2.x -Task 1.3 KB Loader β†’ Required by Task 2.x -Task 1.4 Validator β†’ Required by Task 2.x & 3.x - ↓ -Task 2.x LLM-Router β†’ Required by Task 3.x - ↓ -Task 3.x RoutingWorkflow β†’ Required by Task 4.x & 5.x - ↓ -Task 4.x API/CLI β†’ Can run in parallel with Task 5.x - ↓ -Task 5.x Testing β†’ Gate to Task 6.x - ↓ -Task 6.x Documentation β†’ Final deliverable -``` - ---- - -## RESOURCE ALLOCATION (2-3 Engineers) - -### Engineer 1: Core Workflows -- Tasks: 1.1-1.4 (Foundation) -- Tasks: 2.1-2.5 (LLM-Router) -- Tasks: 3.1-3.6 (RoutingWorkflow) - -### Engineer 2: API/CLI/Testing -- Tasks: 4.1-4.4 (API/CLI) -- Tasks: 5.1-5.4 (Testing) -- Tasks: 6.1-6.4 (Documentation) - -### Both Engineers -- Daily sync on blockers -- Pair programming for complex tasks (2.3, 3.4) -- Code reviews - ---- - -## SUCCESS CRITERIA (Acceptance for Each Phase) - -**Phase 1**: All types compile, KB loads, validator catches errors βœ… -**Phase 2**: llm-router generates valid specs, detects cron βœ… -**Phase 3**: RoutingWorkflow executes any spec, handles errors βœ… -**Phase 4**: API/CLI functional, all endpoints work βœ… -**Phase 5**: >90% test coverage, all scenarios pass βœ… -**Phase 6**: Complete documentation, can deploy βœ… - ---- - -## BLOCKERS TO WATCH - -1. **LLM Integration** (Task 2.3) - - Need: Memory service API working - - Risk: LLM quality - - Mitigation: Test with real LLM early - -2. **Temporal Cron Support** (Task 3.4) - - Need: CronSchedule in StartWorkflowOptions - - Risk: Version compatibility - - Mitigation: Verify Temporal SDK version - -3. **Activity Registration** (Task 3.5) - - Need: All activities implemented - - Risk: Missing activities - - Mitigation: Create stub activities first - -4. **Performance** (Task 5.4) - - Need: <200ms submit latency - - Risk: RoutingWorkflow overhead - - Mitigation: Profile early and often - ---- - -## DELIVERABLES - -### Code Deliverables -- βœ… internal/routing/ (types, validators, executors) -- βœ… statemachine/routing_workflow.go -- βœ… cmd/worker/activities/llm_router.go -- βœ… cmd/api-server/ (full HTTP server) -- βœ… cmd/cli/ (full CLI) -- βœ… tests/ (unit, integration, e2e, load tests) - -### Documentation Deliverables -- βœ… docs/API.md -- βœ… docs/CLI.md -- βœ… docs/DEPLOYMENT.md -- βœ… docs/USER_GUIDE.md -- βœ… docs/examples/ (10+ workflow examples) - -### Operational Deliverables -- βœ… internal/routing/activity_knowledge_base.json -- βœ… k8s/api-server-deployment.yaml -- βœ… Docker image for api-server - ---- - -## DONE CRITERIA - -- [ ] All code compiles without warnings -- [ ] All tests pass (unit, integration, e2e, load) -- [ ] >90% code coverage -- [ ] Documentation complete -- [ ] Can deploy to Kubernetes -- [ ] Can submit workflow from API/CLI -- [ ] Can create cron workflow -- [ ] Performance targets met -- [ ] No critical bugs - -This is **ready to implement**! πŸš€ - diff --git a/MEMORY_ACTIVITIES.md b/MEMORY_ACTIVITIES.md deleted file mode 100644 index 33dd3b8..0000000 --- a/MEMORY_ACTIVITIES.md +++ /dev/null @@ -1,498 +0,0 @@ -# Poimen Memory Service β€” Temporal Activities Integration - -## Summary - -Memory service fully integrated as **Temporal Activities** for workflows. All operations (create, update, retrieve, diagnose) are now first-class Temporal activities with retries, timeouts, logging, and error handling. - -**Status**: βœ… 23/23 tests passing, 10 activities implemented, production-ready. - ---- - -## What Changed - -### Before -```go -// Raw service calls (no Temporal integration) -svc := memory.NewService(...) -id, err := svc.CreateKnowledge(ctx, record) -``` - -### After -```go -// Temporal activity (automatic retries, logging, observability) -id, err := memory.ExecuteCreateKnowledge(ctx, record, nil) -// With custom retry policy: -opts := &memory.ActivityOptions{ - RetryAttempts: 5, - RetryBackoff: time.Second, -} -id, err := memory.ExecuteCreateKnowledge(ctx, record, opts) -``` - ---- - -## Activities Implemented - -| Activity | Purpose | Input | Output | Retries | -|----------|---------|-------|--------|---------| -| **CreateKnowledgeActivity** | Create L1/L2/reference records | `KnowledgeRecord` | `string` (ID) | 3x default | -| **UpdateKnowledgeActivity** | Update existing knowledge | `KnowledgeRecord` | `string` (ID) | 3x | -| **SearchKnowledgeActivity** | Hybrid search (semantic+lexical) | `string` query, `RetrievalOptions` | `[]KnowledgeRecord` | 3x | -| **GetContextActivity** | Three-tier retrieval (Tier 1β†’2β†’3) | tool, task, budget | `*ServiceContext` | 3x | -| **GetVaultActivity** | Browse vault files | (none) | `[]VaultInfo` | 3x | -| **HealthCheckActivity** | Check service health | (none) | `bool` | 3x | -| **LearnFromExecutionActivity** | Learn from task results | taskID, result, tags | `string` (ID) | 3x | -| **DiagnoseIssueActivity** | Diagnose tool/task issues | tool, issue | `[]string` (recommendations) | 3x | -| **AnalyzeErrorActivity** | Analyze errors, find solutions | errorMsg | `[]KnowledgeRecord` | 3x | -| **DocumentDecisionActivity** | Record workflow decisions | decisionType, decision, reasoning | `string` (ID) | 3x | - ---- - -## Setup - -### 1. Register in Worker - -```go -import "github.com/rockliang/poimen/workflows/internal/memory" - -// In worker setup -svc := memory.NewService(baseURL, token, project) -memory.RegisterMemoryActivities(w, svc) -``` - -### 2. Use in Workflows - -```go -func MyWorkflow(ctx workflow.Context) error { - // Simple call (default retry policy) - id, err := memory.ExecuteCreateKnowledge( - ctx, - &memory.KnowledgeRecord{ - Level: "L1", - Content: "...", - }, - nil, // Use defaults - ) - if err != nil { - return err - } - - // Custom retry policy - recommendations, err := memory.ExecuteDiagnoseIssue( - ctx, - "kubectl", - "pod-crash", - &memory.ActivityOptions{ - RetryAttempts: 5, - RetryBackoff: time.Second * 2, - }, - ) - return err -} -``` - ---- - -## Package Structure - -``` -internal/memory/ -β”œβ”€β”€ activities.go (240 lines) β€” Activity implementations -β”œβ”€β”€ activities_test.go (320 lines) β€” 10 activity tests -β”œβ”€β”€ worker_setup.go (310 lines) β€” Registration + wrappers + retry config -β”œβ”€β”€ workflow_examples.go (260 lines) β€” 8 workflow patterns -β”œβ”€β”€ client.go (250 lines) β€” HTTP client (unchanged) -β”œβ”€β”€ service.go (180 lines) β€” High-level wrapper (unchanged) -β”œβ”€β”€ client_test.go (150 lines) β€” Client tests (unchanged) -β”œβ”€β”€ service_test.go (170 lines) β€” Service tests (unchanged) -β”œβ”€β”€ README.md (400 lines) β€” Full API + examples -└── example_activity.go (130 lines) β€” Legacy examples (deprecated) -``` - ---- - -## Activity Features - -### Automatic Retries - -Each activity retries on failure (default 3 attempts, exponential backoff): - -```go -RetryPolicy: &temporal.RetryPolicy{ - InitialInterval: backoff, - BackoffCoefficient: 2.0, - MaximumInterval: 30 * time.Second, - MaximumAttempts: 3, - NonRetryableErrorTypes: [], -} -``` - -### Configurable Timeouts - -Per-activity timeout control: - -```go -opts := &memory.ActivityOptions{ - RetryAttempts: 5, - RetryBackoff: time.Second, - StartTimeout: 30 * time.Second, - HeartbeatRate: 10 * time.Second, -} -``` - -### Built-in Logging - -All activities log: -- Activity start + parameters -- Success + result -- Errors + stack trace - -Example log output: -``` -INFO Creating knowledge title="Pod Debugging" -INFO Knowledge created id=chunk-123 -ERROR Failed to create knowledge error="connection refused" -``` - -### Health Monitoring - -Activities can check service health: - -```go -healthy, err := memory.ExecuteHealthCheck(ctx, nil) -if !healthy { - return fmt.Errorf("memory service unavailable") -} -``` - ---- - -## Workflow Patterns - -### Pattern 1: Learning Workflow - -Learn from task execution, persist knowledge: - -```go -func LearnWorkflow(ctx workflow.Context, taskID string) (string, error) { - result := "Task succeeded" - - knowledgeID, err := memory.ExecuteLearnFromExecution( - ctx, - taskID, - result, - []string{"success"}, - nil, - ) - return knowledgeID, err -} -``` - -### Pattern 2: Diagnostic Workflow - -Diagnose issues, retrieve recommendations: - -```go -func DiagnoseWorkflow(ctx workflow.Context, tool, issue string) ([]string, error) { - return memory.ExecuteDiagnoseIssue( - ctx, - tool, - issue, - &memory.ActivityOptions{RetryAttempts: 5}, - ) -} -``` - -### Pattern 3: Error Recovery - -Analyze error, find recovery path: - -```go -func RecoveryWorkflow(ctx workflow.Context, errorMsg string) ([]string, error) { - records, err := memory.ExecuteAnalyzeError(ctx, errorMsg, nil) - if err != nil { - return nil, err - } - - // Use L1 records (high confidence) - recovery := make([]string, 0) - for _, rec := range records { - if rec.Level == "L1" { - recovery = append(recovery, rec.Content) - } - } - return recovery, nil -} -``` - -### Pattern 4: Context-Aware Decision - -Make decisions based on memory context: - -```go -func ContextualDecisionWorkflow(ctx workflow.Context, tool, task string) (string, error) { - // Get context - svcCtx, err := memory.ExecuteGetContext(ctx, tool, task, 8192, nil) - if err != nil { - return "", err - } - - // Extract best lesson - decision := "" - if len(svcCtx.Lessons) > 0 { - decision = svcCtx.Lessons[0].Text - } - - // Document decision - docID, err := memory.ExecuteDocumentDecision( - ctx, - tool, - decision, - "From memory context", - nil, - ) - return docID, err -} -``` - -### Pattern 5: Multi-Step Workflow - -Multiple memory operations in sequence: - -```go -func MultiStepWorkflow(ctx workflow.Context, topic string) error { - // Step 1: Create knowledge - id, err := memory.ExecuteCreateKnowledge(ctx, &memory.KnowledgeRecord{ - Content: "Initial fact", - }, nil) - if err != nil { - return err - } - - // Step 2: Search related knowledge - records, err := memory.ExecuteSearchKnowledge(ctx, topic, nil, nil) - if err != nil { - return err - } - - // Step 3: Get context - svcCtx, err := memory.ExecuteGetContext(ctx, "workflow", topic, 8192, nil) - if err != nil { - return err - } - - // Step 4: Document findings - _, err = memory.ExecuteDocumentDecision( - ctx, - "workflow_complete", - fmt.Sprintf("Found %d records, tier %d context", len(records), svcCtx.Tier), - "Completed multi-step", - nil, - ) - return err -} -``` - ---- - -## Testing - -All 23 tests pass (10 activity + 13 client/service tests): - -```bash -cd ~/workplace/Poimen/workflows -go test ./internal/memory -v - -# Output: -# === RUN TestActivityCreateKnowledge -# --- PASS: TestActivityCreateKnowledge (0.04s) -# ... -# PASS: 23/23 tests (0.452s) -``` - -### Test Coverage - -**Activity Tests** (10): -- βœ… CreateKnowledgeActivity -- βœ… SearchKnowledgeActivity -- βœ… GetContextActivity -- βœ… DiagnoseIssueActivity -- βœ… AnalyzeErrorActivity -- βœ… HealthCheckActivity -- βœ… LearnFromExecutionActivity -- βœ… DocumentDecisionActivity -- βœ… ActivityOptions -- βœ… ActivityError - -**Client Tests** (5): -- βœ… Ingest -- βœ… Query -- βœ… Context -- βœ… Vault -- βœ… Health - -**Service Tests** (6): -- βœ… CreateKnowledge -- βœ… UpdateKnowledge -- βœ… RetrieveKnowledge -- βœ… RetrieveContext -- βœ… GetVault -- βœ… IsHealthy - ---- - -## Observability - -### Activity Logging - -Automatic logging with activity context: - -``` -INFO Creating knowledge ActivityID=0 ActivityType=CreateKnowledgeActivity Attempt=1 title="Pod Debugging" -INFO Knowledge created ActivityID=0 ActivityType=CreateKnowledgeActivity Attempt=1 id=chunk-123 -ERROR Failed to create knowledge ActivityID=0 ActivityType=CreateKnowledgeActivity Attempt=2 error="service unavailable" -``` - -### Metrics Tracked - -- Activity execution count -- Retry attempts -- Latency per operation -- Success/failure rates -- Timeouts - ---- - -## Error Handling - -### Activity Errors - -All errors include context: - -```go -type MemoryActivityError struct { - ActivityName string - Attempt int - Err error -} - -// Example: "memory activity create-knowledge (attempt 2): connection refused" -``` - -### Retry Strategy - -- Default: 3 attempts, exponential backoff (1s β†’ 2s β†’ 4s β†’ ...) -- Max interval: 30 seconds -- Non-retryable: None (all errors retry) - -Example with custom retry: - -```go -opts := &memory.ActivityOptions{ - RetryAttempts: 5, - RetryBackoff: time.Second, -} -id, err := memory.ExecuteCreateKnowledge(ctx, record, opts) -``` - ---- - -## Performance - -Typical latencies (from logs): - -- CreateKnowledgeActivity: 20-50ms -- SearchKnowledgeActivity: 100-200ms -- GetContextActivity: 150-250ms -- DiagnoseIssueActivity: 100-300ms -- HealthCheckActivity: 10-20ms - -Rate limits (per JWT identity): -- Ingest: 100/hr -- Query: 1000/hr -- Context: 100/hr - ---- - -## Configuration - -### Worker Registration - -```go -// In your worker setup -svc := memory.NewService( - os.Getenv("MEMORY_SERVICE_URL"), - os.Getenv("MEMORY_SERVICE_TOKEN"), - "poimen", -) -memory.RegisterMemoryActivities(w, svc) -``` - -### Environment Variables - -```bash -MEMORY_SERVICE_URL=http://memory-service.poimen.svc.cluster.local:8080 -MEMORY_SERVICE_TOKEN= -``` - -### Activity Defaults - -```go -&memory.ActivityOptions{ - RetryAttempts: 3, - RetryBackoff: time.Second, - StartTimeout: 30 * time.Second, - HeartbeatRate: 10 * time.Second, -} -``` - ---- - -## Files Summary - -| File | Lines | Purpose | -|------|-------|---------| -| `activities.go` | 240 | 10 Temporal activity implementations | -| `activities_test.go` | 320 | Activity unit tests (Temporal test suite) | -| `worker_setup.go` | 310 | Activity registration + wrapper functions + retry config | -| `workflow_examples.go` | 260 | 8 workflow patterns using activities | -| `client.go` | 250 | HTTP client (HTTP layer) | -| `service.go` | 180 | High-level service wrapper | -| `client_test.go` | 150 | HTTP client tests | -| `service_test.go` | 170 | Service tests | -| `README.md` | 400 | Full API documentation + examples | -| **TOTAL** | **2,280** | Production-ready Temporal integration | - ---- - -## Next Steps - -1. **Deploy to cluster**: Update worker Pod to register activities -2. **Use in workflows**: Import and call activities from workflow code -3. **Monitor**: Track activity execution in Temporal UI -4. **Optimize**: Adjust retry policy based on production metrics - ---- - -## Documentation Links - -- Full API: `internal/memory/README.md` -- Workflow patterns: `internal/memory/workflow_examples.go` -- Worker setup: `internal/memory/worker_setup.go` -- Memory service API: `~/workplace/Poimen/memory/CLAUDE.md` - ---- - -## Status - -βœ… **Complete & Production-Ready** - -- 23/23 tests passing -- 10 activities implemented -- Full Temporal integration -- Retry + timeout handling -- Built-in logging -- Error handling -- Documentation complete - -Ready for workflow integration. diff --git a/MEMORY_DRIVEN_ARCHITECTURE.md b/MEMORY_DRIVEN_ARCHITECTURE.md deleted file mode 100644 index 5204c96..0000000 --- a/MEMORY_DRIVEN_ARCHITECTURE.md +++ /dev/null @@ -1,683 +0,0 @@ -# Memory-Driven Architecture for Poimen Workflows - -## Executive Summary - -Poimen state machine (10 phases, 80 tasks, 10 composition gates) will consume Memory Service context & skills to: -- **Learn** from execution attempts (L1 knowledge) -- **Diagnose** failures using memory (three-tier retrieval) -- **Document** decisions for future runs (L2 knowledge) -- **Optimize** prompts with relevant context before agent execution -- **Track** tools, skills, and pattern usage across the harness lifecycle - -This document outlines how Temporal activities integrate with the existing state machine to create a memory-driven, self-improving workflow system. - ---- - -## Current State Machine Architecture - -``` -Poimen Harness (Rust + JSON-RPC) - β”œβ”€ Kernel (Event Log + State Machine) - β”œβ”€ 10 Phases (T0-T10) - β”œβ”€ 80 Tasks (70 build + 10 composition gates) - β”œβ”€ WorkflowDef IR (YAML + Rust builder) - └─ 3 Ports (Verifier, Judge, ModelProvider) -``` - -### Key Components - -**WorkflowDef (IR)**: Canonical hash of workflow definition -- YAML declares: steps, transitions, retry policy, budgets -- Rust implements: verifier logic, judge logic, model behavior - -**State Machine**: Event-sourced, immutable audit trail -- Events: WorkerEvent enum -- Attempts: AttemptState with context partition capture -- Folds: re-derive state from event log - -**Run Executor**: Poll-based with: -- Retry policy per step -- Budget tracking (attempts, tokens, time) -- Context partition per attempt - ---- - -## Memory Service Integration Points - -### Architecture Diagram - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Poimen Workflow β”‚ -β”‚ β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ T1-T10: Task Execution Loop β”‚ β”‚ -β”‚ β”‚ β”‚ β”‚ -β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ -β”‚ β”‚ β”‚ For each step in workflow: β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ 1. RetrieveContext (Memory Service) β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ β”œβ”€ Tool: executor type (planner/judge/impl) β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ β”œβ”€ Task: step name β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ └─ Returns: tier-1 (signature) + tier-2 (ML) β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ 2. OptimizePrompt (with context) β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ β”œβ”€ Add learned facts from memory β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ β”œβ”€ Include skill usage examples β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ └─ Attach budget constraints β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ 3. ExecuteStep (ModelProvider) β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ └─ Agent uses optimized prompt β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ 4. OnStepComplete: β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ β”œβ”€ Success? β†’ LearnFromExecution β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ β”œβ”€ Failure? β†’ AnalyzeError β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ └─ DocumentDecision (all paths) β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ -β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ ↕ β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ Memory Service (PostgreSQL + OpenSearch + Vault) β”‚ β”‚ -β”‚ β”‚ β”‚ β”‚ -β”‚ β”‚ β”œβ”€ L1 Knowledge: Task execution results β”‚ β”‚ -β”‚ β”‚ β”œβ”€ L2 Knowledge: Verified patterns & decisions β”‚ β”‚ -β”‚ β”‚ β”œβ”€ R (Reference): Docs, skill examples, guides β”‚ β”‚ -β”‚ β”‚ └─ Vault: Organized facts by tool/phase/domain β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - -### Memory Service Activities Flow - -``` -Workflow Step Execution β†’ Memory Activities β†’ Response - -1. PRE-EXECUTION (Before step runs) - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ ExecuteGetContext Activity β”‚ - β”‚ β”œβ”€ Input: tool, task, budget β”‚ - β”‚ β”œβ”€ Retrieval: 3-tier β”‚ - β”‚ β”‚ β”œβ”€ Tier 1: Signature match β”‚ - β”‚ β”‚ β”‚ (exact failure patterns) β”‚ - β”‚ β”‚ β”œβ”€ Tier 2: Vector search β”‚ - β”‚ β”‚ β”‚ (learned from similar) β”‚ - β”‚ β”‚ └─ Tier 3: References β”‚ - β”‚ β”‚ (docs, skill guides) β”‚ - β”‚ └─ Returns: Lessons + Skills β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - ↓ - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Prompt Optimization β”‚ - β”‚ β”œβ”€ Add context lessons β”‚ - β”‚ β”œβ”€ Inject skill examples β”‚ - β”‚ └─ Set budget constraints β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - -2. EXECUTION - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Agent executes with context β”‚ - β”‚ (planner/judge/implementer) β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - -3. POST-EXECUTION (After step completes) - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ if SUCCESS: β”‚ - β”‚ ExecuteLearnFromExecution β”‚ - β”‚ β”œβ”€ taskID: step name β”‚ - β”‚ β”œβ”€ result: output β”‚ - β”‚ β”œβ”€ tags: [tool, phase] β”‚ - β”‚ └─ Returns: knowledgeID β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ if FAILURE: β”‚ - β”‚ ExecuteAnalyzeError β”‚ - β”‚ β”œβ”€ errorMsg: failure message β”‚ - β”‚ β”œβ”€ Returns: recovery steps β”‚ - β”‚ └─ (helps with retry) β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ ALWAYS: β”‚ - β”‚ ExecuteDocumentDecision β”‚ - β”‚ β”œβ”€ Type: phase milestone β”‚ - β”‚ β”œβ”€ Decision: action taken β”‚ - β”‚ └─ Reasoning: why chosen β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - ---- - -## Skills and Context in State Machine - -### Skill Types - -**Tool Skills** (Skill Category 1): -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Tool Skills (Executor capabilities) β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ β€’ planner-best-practices β”‚ (T1.3: Plan generation) -β”‚ β€’ judge-evaluation-patterns β”‚ (T1.5: Rubric application) -β”‚ β€’ implementer-code-patterns β”‚ (T2.1: Code generation) -β”‚ β€’ verifier-logic-chains β”‚ (T1.4: Verification) -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - -**Domain Skills** (Skill Category 2): -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Domain Skills (Phase-specific) β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ β€’ T0: State machine kernel β”‚ Event log, fork, rewind -β”‚ β€’ T1: Workflow execution β”‚ Attempt lifecycle, budgets -β”‚ β€’ T2: Error recovery β”‚ Crash matrix, checkpoints -β”‚ β€’ T3: IR canonicalization β”‚ YAML ↔ Rust equivalence -β”‚ β€’ T4-T10: Specialization β”‚ Phase-specific patterns -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - -**Pattern Skills** (Skill Category 3): -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Pattern Skills (Cross-cutting) β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ β€’ retry-strategy β”‚ Exponential backoff -β”‚ β€’ budget-tracking β”‚ Token/attempt/time limits -β”‚ β€’ composition-gates β”‚ Phase completion criteria -β”‚ β€’ schema-evolution β”‚ Backward compatibility -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - -### Context Hierarchy - -``` -WorkflowContext (L0 - Always available) -β”œβ”€ WorkflowDef (IR + hash) -β”œβ”€ PhaseId (T0-T10) -β”œβ”€ StepId (current step) -└─ AttemptState (attempt #, budget) - β”œβ”€ Attempt context (attempt-scoped) - β”œβ”€ Decision points (retry/abort) - └─ Cost ledger (tokens spent) - -TaskContext (L1 - Learned from execution) -β”œβ”€ Tool type (planner/judge/impl) -β”œβ”€ Execution results (input/output) -β”œβ”€ Failure patterns (error signatures) -└─ Retry outcomes (success rates) - -ReferenceContext (L2 - From vault) -β”œβ”€ Skill documentation -β”œβ”€ Best practices (YAML-level) -β”œβ”€ Code patterns (Rust-level) -└─ Design rationale -``` - ---- - -## Activity Usage Per Phase - -### Phase 0-2 (Kernel & Execution Foundation) - -``` -T0: State Machine Kernel -β”œβ”€ GetContextActivity -β”‚ └─ Retrieve lessons on event log patterns -β”œβ”€ LearnFromExecutionActivity -β”‚ └─ Record fold/rewind operations -└─ DocumentDecisionActivity - └─ Track checkpointing decisions - -T1: Attempt Lifecycle -β”œβ”€ GetContextActivity -β”‚ β”œβ”€ Tier 1: Known retry patterns -β”‚ └─ Tier 2: Attempt budget tracking -β”œβ”€ DiagnoseIssueActivity (on failure) -β”‚ β”œβ”€ Search for "budget exhausted" patterns -β”‚ └─ Find recovery step limits -└─ LearnFromExecutionActivity - └─ Record successful attempt patterns - -T2: Error Recovery -β”œβ”€ GetContextActivity -β”‚ └─ Crash matrix lessons -β”œβ”€ AnalyzeErrorActivity -β”‚ β”œβ”€ Match against crash patterns -β”‚ └─ Return recovery procedure -└─ DocumentDecisionActivity - └─ Log recovery action chosen -``` - -### Phase 3-5 (IR & Canonicalization) - -``` -T3: WorkflowDef IR -β”œβ”€ GetContextActivity -β”‚ └─ Tier 1: Canonical hash failures -β”œβ”€ SearchKnowledgeActivity -β”‚ └─ "YAML builder equivalence" patterns -└─ DocumentDecisionActivity - └─ IR versioning decisions - -T4: Schema Evolution -β”œβ”€ GetContextActivity -β”‚ └─ Backward compatibility lessons -β”œβ”€ DiagnoseIssueActivity -β”‚ └─ Upcaster failure diagnosis -└─ LearnFromExecutionActivity - └─ Schema migration successes - -T5: Storage Abstraction -β”œβ”€ SearchKnowledgeActivity -β”‚ └─ DB migration patterns -└─ DocumentDecisionActivity - └─ Storage backend selection -``` - -### Phase 6-8 (Orchestration & APIs) - -``` -T6: Orchestrator -β”œβ”€ GetContextActivity -β”‚ β”œβ”€ Tool: orchestrator -β”‚ β”œβ”€ Task: workflow step dispatch -β”‚ └─ Returns: step ordering lessons -β”œβ”€ SearchKnowledgeActivity -β”‚ └─ Query ordering patterns -└─ LearnFromExecutionActivity - └─ Successful step sequences - -T7: HTTP API -β”œβ”€ DiagnoseIssueActivity (on API error) -β”‚ └─ Match error codes to recovery -└─ DocumentDecisionActivity - └─ Rate limit/timeout decisions - -T8: Observability -β”œβ”€ SearchKnowledgeActivity -β”‚ └─ Logging pattern queries -└─ RefreshMemoryActivity - └─ Periodic metric snapshots -``` - -### Phase 9-10 (Delivery & Completion) - -``` -T9: Deployment -β”œβ”€ GetContextActivity -β”‚ β”œβ”€ Tool: deployment executor -β”‚ └─ Task: artifact rollout -β”œβ”€ DiagnoseIssueActivity (on deployment failure) -β”‚ └─ Canary issues, rollback strategies -└─ AnalyzeErrorActivity - └─ Find deployment-specific solutions - -T10: CLI & Metrics -β”œβ”€ SearchKnowledgeActivity -β”‚ └─ Transcript formatting patterns -β”œβ”€ LearnFromExecutionActivity -β”‚ └─ User interaction patterns -└─ DocumentDecisionActivity - └─ Metric collection decisions -``` - ---- - -## Prompt Optimization with Memory Context - -### Before (Current) - -```go -prompt := fmt.Sprintf(` -Execute step: %s -Workflow: %s -Budget: %d tokens - -Task: %s -`) -``` - -### After (Memory-Optimized) - -```go -// 1. Get context from memory -ctx, err := ExecuteGetContext( - wfCtx, - "planner", // tool type - "T1.3-run-executor", // task name - 4096, // budget -) -if err != nil { - log.Warn("memory unavailable, continue without context") - ctx = nil -} - -// 2. Build prompt with lessons -lessons := "" -if ctx != nil && len(ctx.Lessons) > 0 { - // Add tier-1 (signature matches) - for _, lesson := range ctx.Lessons { - if lesson.Tier == 1 { - lessons += fmt.Sprintf("Known pattern: %s\n", lesson.Text) - } - } -} - -// 3. Inject skills -skills := "" -if ctx != nil && len(ctx.Skills) > 0 { - for _, skill := range ctx.Skills { - skills += fmt.Sprintf("Skill %s: %s\n", skill.Name, skill.Why) - } -} - -// 4. Build optimized prompt -prompt := fmt.Sprintf(` -Execute step: %s -Workflow: %s -Budget: %d tokens - -# Learned Patterns -%s - -# Skills to Apply -%s - -# Instructions -%s -`, stepName, workflowId, budget, lessons, skills, instructions) - -// 5. Send to agent with enriched context -response := agent.Execute(prompt) - -// 6. Learn from result -ExecuteLearnFromExecution( - wfCtx, - stepName, - response.Text, - []string{"phase", "tool", "status"}, -) -``` - ---- - -## Tool Usage Summary - -### Basic Tools - -**Core Workflow Tools**: -- `State Machine Events`: Insert events, compute state -- `WorkflowDef Builder`: Create IR programmatically -- `Run Executor`: Poll and execute steps -- `Attempt Lifecycle`: Retry, checkpoint, rewind - -**Testing Tools**: -- `Harness`: Verification framework -- `Integration Tests`: Phase composition gates -- `Verify Script`: Assertion + diff runner - -### Memory-Integrated Tools - -**New with Memory Service**: -- `ExecuteGetContext`: Retrieve 3-tier context -- `ExecuteLearnFromExecution`: Capture task results -- `ExecuteAnalyzeError`: Diagnosis on failure -- `ExecuteDocumentDecision`: Log milestones -- `ExecuteSearchKnowledge`: Find patterns -- `ExecuteHealthCheck`: Verify service readiness - -**Memory Vault Organization**: -``` -vault/ -β”œβ”€ tools/ -β”‚ β”œβ”€ planner/ -β”‚ β”‚ └─ best-practices.md -β”‚ β”œβ”€ judge/ -β”‚ β”‚ └─ rubric-patterns.md -β”‚ └─ verifier/ -β”‚ └─ logic-chains.md -β”œβ”€ phases/ -β”‚ β”œβ”€ T0-kernel/ -β”‚ β”œβ”€ T1-execution/ -β”‚ └─ T2-recovery/ -β”œβ”€ patterns/ -β”‚ β”œβ”€ retry-strategies.md -β”‚ β”œβ”€ budget-tracking.md -β”‚ └─ error-signatures.md -└─ skills/ - β”œβ”€ schema-evolution.md - β”œβ”€ composition-gates.md - └─ ir-canonicalization.md -``` - ---- - -## State Machine Consumption Model - -### Step Execution with Memory - -```rust -// In RunExecutor::execute_step() - -fn execute_step( - &self, - workflow: &WorkflowDef, - step: &StepId, - attempt: &AttemptState, -) -> Result { - // 1. Pre-execution: Retrieve context - let context = self.memory_svc - .retrieve_context( - "tool_type", // planner/judge/implementer - format!("{:?}", step), // step name - attempt.budget.remaining_tokens, - ) - .await - .ok(); // Fail gracefully if memory unavailable - - // 2. Optimize prompt with memory lessons - let prompt = self.optimize_prompt( - &workflow.def, - step, - context.as_ref(), // Lessons + skills - ); - - // 3. Execute step with agent - let output = self.model_provider.run( - &self.model_id, - &prompt, - &attempt.budget, - ).await?; - - // 4. Post-execution: Learn or diagnose - if output.status == StepStatus::Success { - self.memory_svc - .learn_from_execution( - format!("{:?}", step), - output.text.clone(), - vec!["tool", "phase"], - ) - .await - .ok(); // Non-blocking - } else { - self.memory_svc - .analyze_error( - &output.error_message, - ) - .await - .ok(); // Returns recovery suggestions - } - - // 5. Document decision - self.memory_svc - .document_decision( - "step_completion", - output.text.clone(), - format!("Attempt {}", attempt.number), - ) - .await - .ok(); - - Ok(output) -} -``` - -### Retry Policy Integration - -```rust -// In AttemptState::should_retry() - -fn should_retry(&self, error: &Error) -> bool { - // 1. Check budget first - if self.budget.attempts_remaining == 0 { - return false; - } - - // 2. Consult memory for pattern - let recovery = self.memory_svc - .analyze_error(&error.message) - .await - .ok(); - - // 3. If memory suggests retry strategy, use it - if let Some(recovery_steps) = recovery { - for step in recovery_steps { - if step.level == "L1" { // High confidence - return step.suggests_retry(); - } - } - } - - // 4. Fall back to default policy - self.retry_policy.should_retry(self.number, error) -} -``` - ---- - -## Flow Diagram: Memory-Driven Lifecycle - -``` - Workflow Initiated - β”‚ - ↓ - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Phase T0-T10 β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - ↓ ↓ ↓ - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Get Context β”‚ β”‚ Execute β”‚ β”‚ Analyze β”‚ - β”‚ (Pre-exec) β”‚ β”‚ Step β”‚ β”‚ Result β”‚ - β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ - β”‚ β”‚ β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β†’ β”‚ (optimize) β”‚ - β”‚ β”‚ β”‚ - β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β†’β”‚β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ β”‚ ↓ β”‚ - β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ - β”‚ β”‚ β”‚ Memory Tier β”‚ β”‚ - β”‚ β”‚ β”‚ 1/2/3 β”‚ β”‚ - β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ - β”‚ β”‚ β”‚ - └───┴────────────────────────┴────→ Learn/Document - β”‚ - ↓ - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Continue or Retry?β”‚ - β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - ↓ ↓ - Next Step Attempt Retry - β”‚ (with memory - β”‚ guidance) - β”‚ β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - ↓ - Phase Complete? - β”‚ β”‚ - Yes ↓ No ↓ - β”‚ Return to - Composition Step Loop - Gate - β”‚ - ↓ - All Phases Done? - β”‚ - Yes ↓ No - β”‚ └─→ Next Phase - Workflow - Complete ──→ DocumentDecision - (Final) -``` - ---- - -## Memory-Skills Matrix - -### Which Activities for Which Tools - -``` - β”‚ Planner β”‚ Judge β”‚ Impl β”‚ Verifier β”‚ Executor -─────────┼─────────┼───────┼──────┼──────────┼───────── -Create β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ -Update β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ -Search β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ -Context β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ -Learn β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ -Diagnose β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ -Document β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ -Vault β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ -Health β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ -Analyze β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ -``` - -### Context Availability by Phase - -``` -Phase β”‚ L0 (Workflow) β”‚ L1 (Task) β”‚ L2 (Reference) -──────┼───────────────┼───────────┼──────────────── -T0-1 β”‚ High β”‚ Growing β”‚ Available -T2-3 β”‚ High β”‚ High β”‚ High -T4-6 β”‚ High β”‚ High β”‚ Very High -T7-10 β”‚ High β”‚ Very Highβ”‚ Very High -``` - ---- - -## Next Steps - -### Phase 1: Integration (This Sprint) -- βœ… Memory activities implemented (12 activities) -- βœ… Temporal test suite passing (23/23 tests) -- πŸ”„ Wire activities into RunExecutor -- πŸ”„ Add memory pre/post-execution hooks -- πŸ”„ Ingest skill YAML β†’ memory vault - -### Phase 2: Optimization (Next Sprint) -- πŸ”„ Prompt optimization with context -- πŸ”„ Retry policy enhancement via memory -- πŸ”„ Budget tracking with learned limits -- πŸ”„ Phase composition gate improvements - -### Phase 3: Observability (2 Sprints) -- πŸ”„ Memory usage metrics per phase -- πŸ”„ Context relevance scoring -- πŸ”„ Skill suggestion effectiveness tracking -- πŸ”„ Orchestrator dashboard with memory stats - ---- - -## Summary - -Memory-driven architecture enables Poimen to: - -1. **Learn** from every execution (Tier 1 knowledge) -2. **Improve** prompts with context (Tier 2/3 lessons) -3. **Recover** from failures faster (diagnose + suggest) -4. **Document** decisions for compliance (audit trail) -5. **Organize** skills and patterns (vault by domain) -6. **Scale** across phases (cross-phase pattern reuse) - -The state machine becomes a **learning system**, not just an executorβ€”every run improves future runs. diff --git a/MEMORY_INTEGRATION.md b/MEMORY_INTEGRATION.md deleted file mode 100644 index c118838..0000000 --- a/MEMORY_INTEGRATION.md +++ /dev/null @@ -1,373 +0,0 @@ -# Poimen Memory Service Integration - -## Overview - -Poimen workflows now integrate with the **Poimen Memory Service** for: -- βœ… **Create** knowledge records (L1/L2/reference) -- βœ… **Update** existing knowledge -- βœ… **Retrieve** knowledge via hybrid search -- βœ… **Context** retrieval (three-tier: signature β†’ vector β†’ reference) - -Package: `internal/memory` β†’ 4 files, 15+ tests, 100% passing - ---- - -## Architecture - -``` -Workflow Activity - ↓ -Service (high-level) - ↓ -Client (low-level HTTP) - ↓ -Memory Service API (remote) - β”œβ”€ POST /memory/ingest (create knowledge) - β”œβ”€ POST /memory/query (search) - β”œβ”€ POST /memory/context (three-tier retrieval) - β”œβ”€ GET /memory/vault (browse) - └─ GET /health (health check) -``` - ---- - -## Quick Start - -### 1. Import - -```go -import "github.com/rockliang/poimen/workflows/internal/memory" -``` - -### 2. Create Service - -```go -svc := memory.NewService( - "http://memory-service.poimen.svc.cluster.local:8080", - "jwt-token-from-env", - "poimen", // project -) -``` - -### 3. Create Knowledge - -```go -id, err := svc.CreateKnowledge(ctx, &memory.KnowledgeRecord{ - Level: "L1", - Content: "Pod debugging: kubectl logs ", - Source: "workflow://task-123", -}) -``` - -### 4. Search Knowledge - -```go -records, err := svc.RetrieveKnowledge(ctx, "pod debugging", nil) -for _, rec := range records { - fmt.Println(rec.Content) -} -``` - -### 5. Get Context - -```go -svcCtx, err := svc.RetrieveContext(ctx, "kubectl", "debug-pod", 8192) -for _, lesson := range svcCtx.Lessons { - fmt.Println(lesson.Text) -} -``` - ---- - -## Files Added - -``` -internal/memory/ -β”œβ”€β”€ client.go (HTTP client, 250 lines) -β”œβ”€β”€ client_test.go (6 tests) -β”œβ”€β”€ service.go (High-level API, 180 lines) -β”œβ”€β”€ service_test.go (5 tests) -β”œβ”€β”€ example_activity.go (Workflow integration examples) -└── README.md (Full API docs) -``` - -### File Purposes - -| File | Purpose | -|------|---------| -| `client.go` | Low-level HTTP client for memory API endpoints | -| `service.go` | High-level wrapper with project-scoped operations | -| `example_activity.go` | Temporal workflow activity examples | -| `client_test.go` | Client unit tests (mock HTTP server) | -| `service_test.go` | Service unit tests | -| `README.md` | Complete API reference + examples | - ---- - -## Test Results - -``` -βœ… TestClientIngest (Create) -βœ… TestClientQuery (Search) -βœ… TestClientContext (Three-tier retrieval) -βœ… TestClientVault (Browse) -βœ… TestClientHealth (Health check) -βœ… TestServiceCreateKnowledge -βœ… TestServiceRetrieveKnowledge -βœ… TestServiceRetrieveContext -βœ… TestServiceGetVault -βœ… TestServiceIsHealthy -βœ… TestServiceUpdateKnowledge - -PASS: 11/11 tests (0.317s) -``` - ---- - -## API Endpoints Covered - -| Endpoint | Method | Wrapper | Status | -|----------|--------|---------|--------| -| `/memory/ingest` | POST | `CreateKnowledge()` | βœ… Implemented | -| `/memory/query` | POST | `RetrieveKnowledge()` | βœ… Implemented | -| `/memory/context` | POST | `RetrieveContext()` | βœ… Implemented | -| `/memory/vault` | GET | `GetVault()` | βœ… Implemented | -| `/health` | GET | `IsHealthy()` | βœ… Implemented | - ---- - -## Usage Examples - -### Example 1: Learn from Task Execution - -```go -// In Temporal workflow/activity: -result := executeTask() -id, err := svc.CreateKnowledge(ctx, &memory.KnowledgeRecord{ - Level: "L1", - Title: "Task Result", - Content: result, - Source: "workflow://task-id", -}) -``` - -### Example 2: Diagnose Issue - -```go -// Retrieve context for debugging -svcCtx, err := svc.RetrieveContext(ctx, "kubectl", "pod-crash", 8192) -for _, lesson := range svcCtx.Lessons { - fmt.Printf("Tier %d: %s\n", lesson.Tier, lesson.Text) -} -for _, skill := range svcCtx.Skills { - fmt.Printf("Skill: %s\n", skill.Name) -} -``` - -### Example 3: Search Knowledge - -```go -records, err := svc.RetrieveKnowledge(ctx, "kubernetes debugging", &memory.RetrievalOptions{ - Limit: 10, - LevelFilter: []string{"L1", "L2"}, - Floor: 0.7, // Minimum relevance -}) -``` - -### Example 4: Update Knowledge - -```go -_, err := svc.UpdateKnowledge(ctx, &memory.KnowledgeRecord{ - ID: "chunk-123", - Level: "L2", - Content: "Updated facts...", -}) -``` - ---- - -## Workflow Integration Pattern - -### Pattern 1: Learning Workflow - -```go -type LearnWorkflow struct { - MemoryService *memory.Service -} - -func (w *LearnWorkflow) Run(ctx context.Context, task string) error { - // Execute task - result, err := executeTask(task) - if err != nil { - return err - } - - // Learn from result - _, err = w.MemoryService.CreateKnowledge(ctx, &memory.KnowledgeRecord{ - Content: result, - Source: "workflow://learn/" + task, - }) - return err -} -``` - -### Pattern 2: Diagnostic Workflow - -```go -func (w *Workflow) Diagnose(ctx context.Context, tool, issue string) error { - // Retrieve context - svcCtx, err := w.MemoryService.RetrieveContext(ctx, tool, issue, 8192) - if err != nil { - return err - } - - // Use best lesson (tier-1 has highest confidence) - if len(svcCtx.Lessons) > 0 { - lesson := svcCtx.Lessons[0] - fmt.Printf("Recommended action: %s\n", lesson.Text) - } - - return nil -} -``` - -### Pattern 3: Search-Based Workflow - -```go -func (w *Workflow) SearchAndApply(ctx context.Context, query string) error { - records, err := w.MemoryService.RetrieveKnowledge(ctx, query, nil) - if err != nil { - return err - } - - for _, rec := range records { - if rec.Level == "L1" { // High confidence - applyKnowledge(rec.Content) - } - } - - return nil -} -``` - ---- - -## Configuration - -### Environment Variables - -```bash -# Memory service endpoint -MEMORY_SERVICE_URL=http://memory-service.poimen.svc.cluster.local:8080 - -# JWT token (from Authentik) -MEMORY_SERVICE_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGc... - -# Project name -MEMORY_PROJECT=poimen -``` - -### Initialization - -```go -// From environment -svc := memory.NewService( - os.Getenv("MEMORY_SERVICE_URL"), - os.Getenv("MEMORY_SERVICE_TOKEN"), - os.Getenv("MEMORY_PROJECT"), -) - -// Or hardcoded (for testing) -svc := memory.NewService( - "http://localhost:8080", - "test-token", - "poimen", -) -``` - ---- - -## Error Handling - -Common errors: - -| Error | Cause | Solution | -|-------|-------|----------| -| 401 Unauthorized | Invalid/missing JWT | Check token in env | -| 403 Forbidden | Token lacks capability | Ensure token has `memory:read`/`memory:write` | -| 429 Too Many Requests | Rate limit exceeded | Implement backoff | -| 503 Service Unavailable | Memory service down | Retry with exponential backoff | -| Timeout | Slow network/remote | Increase timeout or retry | - -Example with retry: - -```go -var lastErr error -for attempt := 0; attempt < 3; attempt++ { - resp, err := svc.RetrieveKnowledge(ctx, query, nil) - if err == nil { - return resp, nil - } - lastErr = err - time.Sleep(time.Duration(math.Pow(2, float64(attempt))) * time.Second) -} -return nil, lastErr -``` - ---- - -## Performance Notes - -- **Query**: ~150ms (hybrid search) -- **Context**: ~200ms (three-tier retrieval) -- **Ingest**: ~10ms (sync), async processing -- **Vault**: ~50ms (file listing) - -Rate limits: -- Ingest: 100/hour -- Query: 1000/hour -- Context: 100/hour - ---- - -## Testing - -### Run Tests - -```bash -cd ~/workplace/Poimen/workflows -go test ./internal/memory -v -``` - -### Mock Integration - -Tests use `httptest.NewServer` for mocking. Example: - -```go -server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(QueryResponse{...}) -})) -defer server.Close() - -client := memory.NewClient(server.URL, "test-token") -resp, _ := client.Query(context.Background(), &QueryRequest{...}) -``` - ---- - -## Next Steps - -1. **Add to Temporal activities**: Integrate into workflow activities -2. **Configure JWT token**: Set env var in deployment -3. **Add error handling**: Implement retry logic -4. **Monitor usage**: Track API calls, response times -5. **Extend patterns**: Add domain-specific activities - ---- - -## References - -- Memory service API: `~/workplace/Poimen/memory/CLAUDE.md` -- Package API docs: `internal/memory/README.md` -- Example activities: `internal/memory/example_activity.go` diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index a97c87c..0000000 --- a/PLAN.md +++ /dev/null @@ -1,337 +0,0 @@ -# Multi-Agent Dev Orchestrator (Temporal + Go) β€” Implementation Plan - -See the full design doc at `/Users/rockliang/.claude/plans/considered-u-are-a-curried-reef.md`. - -## Quick Summary - -Build a Temporal-based orchestrator that drives multi-agent software dev work on arbitrary target repos. Three roles (Planner/reasoning, Judge/reasoning, Implementer/cheaper model) collaborate on hierarchical tasks (`T0` milestone split into `T0.1`-`T0.9` subtasks). Orchestrator owns config (system prompt, skills, activity timeouts/retries), live-updatable via signals. All code runs on shared FS where target repo sits; git concurrency handled via worktrees + advisory lock. System testable with mocked activities + real end-to-end against `temporal.riotpiao.com`. - -## Implementation Track - -**Milestone T0**: Nine subtasks, each with its own verification gate = completion criterion. - -| Task | Scope | Status | -|---|---|---| -| [T0.1](#t01-repo-scaffold) | Repo scaffold: `go.mod`, `statemachine/`, `action/`, `cmd/`, `prompts/`, `internal/`, `tests/` | [ ] | -| [T0.2](#t02-shared-types) | Shared types: `ModelSpec`, `PromptSpec`, `OrchestratorConfig`, `ActivityTuning`, `PiRetryPolicy` | [ ] | -| [T0.3](#t03-git-and-locking) | Git & locking: `CloneRepoActivity`, worktrees, squash-merge, `orchestrator.lock` | [ ] | -| [T0.4](#t04-pi-and-error-classification) | `PrepareSkillsActivity`, `classifyPiErr` (4xx/5xx/504), stream timeout learning | [ ] | -| [T0.5](#t05-llm-agents-and-prompts) | Planner/Judge/Implementer activities, LLM client, prompt templates + live customization | [ ] | -| [T0.6](#t06-taskunit-workflow) | TaskUnit workflow: retry loops, timeout escalation, lessons injection | [ ] | -| [T0.7](#t07-orchestrator-workflow) | Orchestrator workflow: config state, signals, fan-out/fan-in, `continue-as-new`, 504 learning | [ ] | -| [T0.8](#t08-worker-and-starter) | Worker & starter CLIs, env/config loading, Temporal registration | [ ] | -| [T0.9](#t09-end-to-end) | Full e2e against real cluster + scratch repo: all 7 verification items | [ ] | - ---- - -### T0.1: Repo Scaffold - -Create directory structure, `go.mod`, empty stubs. - -**Verification:** `go build ./...` succeeds; layout matches plan. - -
-Details - -``` -/go.mod -/cmd/worker/main.go -/cmd/starter/main.go -/statemachine/types.go types.go signals.go orchestrator.go taskunit.go -/action/planner.go implementer.go judge.go git.go skills.go integration_test.go lessons.go llm/client.go -/prompts/registry.go planner/default.tmpl judge/default.tmpl implementer/default.tmpl -/internal/config/config.go lock/flock.go -/tests/taskunit_workflow_test.go orchestrator_workflow_test.go -``` - -
- ---- - -### T0.2: Shared Types - -Implement `statemachine/types.go` with all config/input/output structs. Document defaults. - -**Verification:** Unit test asserts all defaults (5m / 2s / 30s / 2.0 / 30s stream / 2m stream-max). - -
-Details - -- `ModelSpec`: ModelID, Thinking, Effort -- `PromptSpec`: TemplateRef, RawTemplate, Variables, Model, LessonsRef -- `OrchestratorInput`, `OrchestratorOutput` -- `TaskUnitInput`, `TaskUnitOutput` -- `ActivityTuning`: ImplementerBaseTimeout, ImplementerMaxRetries, JudgeTimeout, PiRetry -- `PiRetryPolicy`: ScheduleToCloseTimeout (5m), InitialInterval (2s), MaximumInterval (30s), BackoffCoefficient (2.0), StreamTimeout (30s), StreamTimeoutMax (2m) -- `OrchestratorConfig`: SystemPrompt, Skills, RolePrompts, Tuning - -
- ---- - -### T0.3: Git & Locking - -Implement `action/git.go` + `internal/lock/flock.go`. - -**Verification:** Test against local scratch repo: clone-if-empty vs fetch-if-exists, worktree lifecycle (add/commit/remove), squash-merge produces exactly one commit on main. - -
-Details - -**Activities:** -- `CloneRepoActivity(ctx, {RemoteURL, TargetRepoPath}) error` β€” idempotent `git clone` or `git fetch` -- `GitWorktreeAddActivity(ctx, {RepoPath, TaskID}) (string, error)` β€” returns worktree path, guarded by lock -- `GitCommitActivity(ctx, {WorktreePath, Message}) error` β€” commits in worktree (no lock needed) -- `GitPushActivity(ctx, {RepoPath}) error` β€” guarded by lock -- `GitSquashMergeActivity(ctx, {RepoPath, Branches, Message}) error` β€” guarded by lock - -**Lock helper (`internal/lock/`):** -- `Lock(path string) error`, `Unlock(path string) error` using `golang.org/x/sys/unix.Flock` or `fcntl` equivalent - -**Squash-merge sequence:** -``` -fetch origin main -checkout main && pull --ff-only origin main -for b in branches: - merge --squash $b -commit -m "T0: squash merge subtasks..." -push origin main -for b in branches: - worktree remove worktrees/$id --force - branch -D $b -``` - -
- ---- - -### T0.4: Pi & Error Classification - -Implement `action/skills.go` with `PrepareSkillsActivity` and `classifyPiErr`. - -**Verification:** Unit test all three error buckets (4xx/5xx/504) against a mocked `pi` HTTP client. - -
-Details - -**`PrepareSkillsActivity`:** -- Input: `{Skills []SkillRef, StreamTimeout time.Duration}` -- For each skill, `pi clone-or-fetch ` (idempotent) -- Each skill guarded by its own lock - -**Error classification:** -```go -func classifyPiErr(err error) error { - // 4xx -> NonRetryableApplicationError "PiClientError" - // 504 -> ApplicationError "PiStreamTimeout" - // others -> retryable -} -``` - -
- ---- - -### T0.5: LLM Agents & Prompts - -Implement LLM activities + prompt templates. - -**Verification:** Unit test renders a `PromptSpec` (system prompt + template override + raw template) and calls mock Anthropic client. - -
-Details - -**Activities:** -- `PlanningActivity(ctx, {OrchestratorConfig, BoardState}) (TaskDispatch, error)` β€” reads board/INDEX.md, calls Planner -- `ImplementerActivity(ctx, {PromptSpec, WortkreeePath, Lessons}) (ImplementOutput, error)` β€” tool-call agent loop -- `JudgeActivity(ctx, {PromptSpec, Diff, IntegrationTestResult}) (Verdict, Critique, error)` β€” reviews correctness -- `RunIntegrationTestActivity(ctx, {WortkreeePath, TestCmd}) (pass/fail, logs, error)` β€” shells out - -**Prompt templates:** -- `planner/default.tmpl`: expects `{{.SystemPrompt}}`, `{{.TaskBoard}}`, etc. -- `judge/default.tmpl`: expects `{{.SystemPrompt}}`, `{{.Diff}}`, `{{.TestResult}}` -- `implementer/default.tmpl`: expects `{{.SystemPrompt}}`, `{{.Task}}`, `{{.Lessons}}` - -**`prompts/registry.go`:** -- `go:embed prompts/*.tmpl` -- `Render(templateRef string, variables map[string]any) (string, error)` - -**`action/llm/client.go`:** -- Thin Anthropic client wrapper -- Read `ANTHROPIC_API_KEY` from env -- Call `messages.Create` with model/thinking/effort from `ModelSpec` - -
- ---- - -### T0.6: TaskUnit Workflow - -Implement `statemachine/taskunit.go` with retry loops & timeout escalation. - -**Verification (Testsuite):** -- Pass-first-try -- Fail-then-pass-after-lesson-injection -- Retries-exhausted -- Timeout-escalation (both judges) - -
-Details - -**Flow:** -1. `GitWorktreeAddActivity` β†’ get isolated working tree -2. Retry loop: - - Track `timeoutAttempt`, `judgeAttempt` separately - - `ImplementerActivity` with timeout = `BaseTimeout * timeoutAttempt` - - If timeout, increment `timeoutAttempt` and retry (duration grows) - - If success, call `RunIntegrationTestActivity` - - Call `JudgeActivity` - - If judge pass, commit in worktree and return - - If judge fail, append to lessons, increment `judgeAttempt`, retry (lessons injected next time) - - If retries exhausted, return fail verdict to orchestrator - -**Key detail:** HeartbeatTimeout = (BaseTimeout * timeoutAttempt) / 4, scales with escalation. - -
- ---- - -### T0.7: Orchestrator Workflow - -Implement `statemachine/orchestrator.go` with config state, signals, fan-out/fan-in, `continue-as-new`, 504 learning. - -**Verification (Testsuite):** -- Fan-out/fan-in correctness -- Squash-merge triggers on submilestone complete -- `continue-as-new` at cycle cap, carries `OrchestratorConfig` forward -- `update-*` signals mutate config without touching in-flight TaskUnit -- `PiStreamTimeout` doubles `StreamTimeout` and persists it - -
-Details - -**Per-cycle logic:** -1. If `config.Skills` changed, call `PrepareSkillsActivity` once (wraps it for 504 learning) -2. Call `PlanningActivity` β†’ get dispatch decision -3. Fan out: `workflow.ExecuteChildWorkflow(TaskUnitWorkflow, ...)` for each dispatched T0.x -4. Await all via `workflow.Selector` -5. Call `PlanningActivity` again to update board + commit + push -6. If submilestone complete, call `GitSquashMergeActivity` -7. Increment cycle count -8. If cycle count >= cap, `workflow.NewContinueAsNewError(ctx, ..., nextInput)` - -**Signal handlers:** -- `pause`, `resume`: gate the cycle loop -- `abort-task(taskID)`: forward via `SignalExternalWorkflow` to TaskUnit -- `inject-lesson`: append to lessons store -- `update-system-prompt`, `update-skills`, `update-role-prompt`, `update-tuning`: mutate `config.*` - -**504 learning wrapper (pseudo-code):** -```go -for { - r := config.Tuning.PiRetry - err := ExecuteActivity(..., PrepareSkillsActivity, Input{...StreamTimeout: r.StreamTimeout}) - if isPiStreamTimeout(err) && r.StreamTimeout < r.StreamTimeoutMax { - config.Tuning.PiRetry.StreamTimeout *= 2 - continue - } - break -} -``` - -
- ---- - -### T0.8: Worker & Starter CLIs - -Implement `cmd/worker/main.go` and `cmd/starter/main.go`. - -**Verification:** -- `go run ./cmd/worker` connects to `temporal.riotpiao.com:7233` without error -- `go run ./cmd/starter --dry-run` starts a workflow that appears in Temporal Web UI - -
-Details - -**`cmd/worker/main.go`:** -```go -config := loadConfig() // reads env: TEMPORAL_NAMESPACE, TEMPORAL_TLS_CERT, TEMPORAL_TLS_KEY, ANTHROPIC_API_KEY -c, err := client.Dial(client.Options{HostPort: "temporal.riotpiao.com:7233", ...TLS...}) -w, err := worker.New(c, "default", worker.Options{}) -// register both workflows -w.RegisterWorkflow(statemachine.OrchestratorWorkflow) -w.RegisterWorkflow(statemachine.TaskUnitWorkflow) -// register all activities -w.RegisterActivity(action.CloneRepoActivity) -w.RegisterActivity(action.GitWorktreeAddActivity) -// ... etc -w.Run() -``` - -**`cmd/starter/main.go`:** -```go -flag.String("repo", "", "target repo path") -flag.String("remote", "", "remote URL") -flag.String("milestone", "T0", "milestone ID") -flag.Bool("dry-run", false, "disable git push/merge") -flag.String("planner-model", "claude-opus-5", "planner model ID") -// ... judge, implementer models -// build OrchestratorInput, call client.ExecuteWorkflow -``` - -**`internal/config/config.go`:** -- Load Temporal settings from env -- Load ANTHROPIC_API_KEY from env -- Return filled config struct - -
- ---- - -### T0.9: End-to-End Test - -Run against real `temporal.riotpiao.com` + disposable scratch repo. - -**Verification (all 7 items in the plan):** -1. Clone bootstrap: fresh clone when repo path empty -2. Full cycle: dispatch subtasks, judge pass/fail, commit, squash-merge -3. Live signal updates: change prompt/skills mid-run, next dispatch sees them -4. 5xx retry-then-succeed + always-503 exhausts at 5m mark -5. 504 stream-timeout learning: doubles and is actually used, capped at max -6. `continue-as-new` history bounded -7. Squash-merge result: main has one squashed commit per submilestone - -
-Details - -**Fixture repo structure:** -``` -tasks/ - INDEX.md (guidelines) - board.json (task list, T0.1-T0.3 with trivial definitions) -``` - -**Example subtask:** "Create file `output.txt` with content 'hello world'" - -**Run sequence:** -1. `go run ./cmd/starter --repo /tmp/fixture --remote git@forgejo.homelab:scratch/workflow-test.git --dry-run` -2. Monitor Temporal Web UI for workflow progress -3. Midway, send signals: `temporal workflow signal --workflow-id orch-... --name update-role-prompt ...` -4. Confirm next dispatch uses new prompt (assert marker in output file) -5. Confirm board updated, lessons file exists (if any failure happened) -6. Remove `--dry-run`, repeat against real remote -7. Assert final state: real commits on remote, squash-merge on main - -
- ---- - -## Next Steps - -1. Approve this scaffold (PLAN.md + tasks/INDEX.md + board) -2. Start T0.1 β†’ checkout branch `task/T0.1` β†’ scaffold repo structure -3. Each task: implement, test locally, verify against criterion -4. Mark on board: [x] when verification passes -5. T0.9: final e2e run -6. Squash all T0.* branches into main, push diff --git a/PROGRESS_STATUS.md b/PROGRESS_STATUS.md deleted file mode 100644 index 05b3e74..0000000 --- a/PROGRESS_STATUS.md +++ /dev/null @@ -1,170 +0,0 @@ -# Poimen Routing Workflow - Implementation Progress - -**Last Updated**: August 31, 2025 -**Current Phase**: Phase 1: Foundation -**Overall Progress**: 1 of 27 tasks complete (3.7%) - ---- - -## PHASE 1: FOUNDATION (8-10 hours) - -### Task 1.1: Create Go Type Definitions βœ… COMPLETE - -**Status**: COMPLETE -**Completed**: 2025-08-31 -**Hours Used**: 2 hours -**Effort Estimate**: 2 hours - -**Deliverables**: -- βœ… `internal/routing/types.go` (159 lines) - - WorkflowSpec (one-time workflows) - - CronWorkflowSpec (scheduled workflows) - - State (Task/Pass/Fail) - - RetryPolicy, CatchClause - - ExecutionContext, ExecutionEvent - - PollParams, PollResult, Result - - ActivityMetadata, InputField, OutputField - - Constraints, Heartbeat - -- βœ… `internal/routing/types_test.go` (286 lines) - - 8 comprehensive unit tests - - JSON marshaling/unmarshaling - - Complex workflow scenarios - - All tests PASS βœ… - -**Acceptance Criteria**: -- βœ… All types compile without errors -- βœ… JSON marshaling/unmarshaling works correctly -- βœ… Unit tests pass (9/9 PASS) -- βœ… Ready for next phase - -**Commit**: `05a150b1` - feat(routing): implement WorkflowSpec and CronWorkflowSpec types - -**Notes**: Types are solid and flexible. Ready to proceed with Task 1.2 (ActivityKnowledgeBase.json) - ---- - -### Task 1.2: Create ActivityKnowledgeBase.json ⏳ TODO - -**Status**: TODO -**Effort Estimate**: 3 hours -**Blocker**: None - -**What to build**: -- ActivityKnowledgeBase.json with ~7-10 activities -- Each activity: name, description, category, inputs, outputs, constraints -- Include both flaky (retry 3x) and stable (retry 1x) activities - ---- - -### Task 1.3: Create ActivityKnowledgeBase Loader ⏳ TODO - -**Status**: TODO -**Effort Estimate**: 2 hours -**Blocker**: Depends on Task 1.2 - ---- - -### Task 1.4: Create WorkflowSpec Validator ⏳ TODO - -**Status**: TODO -**Effort Estimate**: 3 hours -**Blocker**: Depends on Task 1.1 βœ… - ---- - -## PHASE 2: LLM-ROUTER (12-15 hours) - -### Task 2.1-2.5 ⏳ TODO - -**Status**: All TODO -**Blocker**: Waiting on Phase 1 completion - ---- - -## PHASE 3: ROUTINGWORKFLOW (15-18 hours) - -### Task 3.1-3.6 ⏳ TODO - -**Status**: All TODO -**Blocker**: Waiting on Phase 1 & 2 completion - ---- - -## PHASE 4: API/CLI (12-15 hours) - -### Task 4.1-4.4 ⏳ TODO - -**Status**: All TODO -**Blocker**: Waiting on Phase 3 completion - ---- - -## PHASE 5: TESTING (8-12 hours) - -### Task 5.1-5.4 ⏳ TODO - -**Status**: All TODO -**Blocker**: Waiting on Phase 4 completion - ---- - -## PHASE 6: DOCUMENTATION (5-8 hours) - -### Task 6.1-6.4 ⏳ TODO - -**Status**: All TODO -**Blocker**: Waiting on Phase 5 completion - ---- - -## SUMMARY - -**Completed**: 1/27 tasks (3.7%) -**In Progress**: 0 tasks -**Blocked**: 0 tasks -**Remaining**: 26 tasks (96.3%) - -**Phase 1 Completion**: 25% (1 of 4 tasks done) -**Estimated Time to Phase 1 Done**: 6-8 hours (by tomorrow) -**Estimated Time to All Done**: 60-70 hours (3-4 weeks) - -**Current Velocity**: 2 hours/task -**Est. Daily Capacity**: 8 hours/day -**Est. Days to Completion**: 8-10 days (assuming 1 engineer, 8h/day) - ---- - -## NEXT IMMEDIATE TASKS - -**Priority 1** (TODAY if possible): -- [ ] Task 1.2: Create ActivityKnowledgeBase.json (3h) -- [ ] Task 1.3: Create KB Loader (2h) - -**Priority 2** (TOMORROW): -- [ ] Task 1.4: Create Validator (3h) -- [ ] Phase 1 sign-off complete - -**Priority 3** (BEGIN Phase 2): -- [ ] Task 2.1: JSONPath Resolver (3h) -- [ ] Task 2.2: llm-router skeleton (2h) - ---- - -## BLOCKERS & NOTES - -None currently. Types are solid and ready for next phase. - ---- - -## COMMITS THIS SESSION - -| Commit | Message | Files | -|--------|---------|-------| -| 05a150b1 | feat(routing): implement WorkflowSpec and CronWorkflowSpec types | types.go, types_test.go | - ---- - -**Status Indicator**: -🟒 ON TRACK - Phase 1 started, Task 1.1 complete, no blockers - diff --git a/README_IMPLEMENTATION.md b/README_IMPLEMENTATION.md deleted file mode 100644 index 324d326..0000000 --- a/README_IMPLEMENTATION.md +++ /dev/null @@ -1,360 +0,0 @@ -# Poimen Routing Workflow - Complete Implementation Guide - -## πŸ“š DOCUMENTATION STRUCTURE - -You now have **4 complete documents** that form a complete specification: - -### 1. `ROUTING_WORKFLOW_SPEC.md` (30+ KB) -**The Technical Specification** - Everything about the system design - -Contains: -- ActivityKnowledgeBase.json format -- llm-router Activity (intelligent workflow generator) -- RoutingWorkflow (generic executor) -- Go type definitions (copy-paste ready) -- Implementation architecture -- State types (Task, Pass, Fail) -- CronWorkflowSpec (scheduled workflows) -- Execution flow examples -- Cron syntax reference - -**When to use**: Building the system, understanding architecture - ---- - -### 2. `CRON_JOBS_QUICK_REFERENCE.md` (4 KB) -**Quick Reference for Cron Jobs** - -Contains: -- Cron syntax examples -- How llm-router detects scheduled jobs -- Execution tracking -- API endpoints for cron -- One-time vs Cron comparison - -**When to use**: Testing cron features, quick lookup - ---- - -### 3. `IMPLEMENTATION_TASKS.md` (30+ KB) -**The Complete Task Breakdown** - What to build, in what order - -Contains: -- 27 specific, actionable tasks -- Effort estimates per task (2-5 hours each) -- Acceptance criteria for each task -- Dependencies between tasks -- Timeline (3 weeks, 1-2 engineers) -- Resource allocation -- Blockers to watch -- Success criteria - -**Structure**: -``` -Phase 1: Foundation (8-10 hours) - β”œβ”€ Task 1.1: Types - β”œβ”€ Task 1.2: Knowledge Base - β”œβ”€ Task 1.3: KB Loader - └─ Task 1.4: Validator - -Phase 2: LLM-Router (12-15 hours) - β”œβ”€ Task 2.1: JSONPath Resolver - β”œβ”€ Task 2.2: Activity Skeleton - β”œβ”€ Task 2.3: Intent Analysis - β”œβ”€ Task 2.4: Spec Builder - └─ Task 2.5: Cron Builder - -Phase 3: RoutingWorkflow (15-18 hours) - β”œβ”€ Task 3.1-3.6: Executors & State Machine - -Phase 4: API/CLI (12-15 hours) - β”œβ”€ Task 4.1-4.4: Handlers, Commands, Validation - -Phase 5: Testing (8-12 hours) - β”œβ”€ Task 5.1-5.4: Unit, Integration, E2E, Load tests - -Phase 6: Docs & Deployment (5-8 hours) - β”œβ”€ Task 6.1-6.4: API.md, CLI.md, Deployment.md, User Guide -``` - -**When to use**: Planning sprints, assigning work, tracking progress - ---- - -### 4. `DESIGN_MASTER_REVIEW.md` (25+ KB) -**Executive Summary for Stakeholders** - -Contains: -- Problem/solution -- 3 patterns (Sequential, Await-Task-Complete, Retry) -- 3 entry points (CLI, API, Legacy) -- Phases 1-5 (52-58 hours) -- KMSvc questions (Q1-Q6) -- Risks & mitigations -- Success criteria -- Approval checklist - -**When to use**: Stakeholder review, getting buy-in, architecture approval - ---- - -## 🎯 THE ARCHITECTURE AT A GLANCE - -``` -User Message: "Analyze repo for security and quality every day at 2 AM" - ↓ -[llm-router Activity] - Reads: ActivityKnowledgeBase.json - Uses LLM to understand intent - Decides: Clone β†’ AnalyzeCode β†’ SecurityScan β†’ Combine β†’ Notify - Decides timeouts, retries from knowledge base - Detects schedule: "0 2 * * *" - Generates: CronWorkflowSpec - ↓ -[RoutingWorkflow] (Generic Executor) - Registers with Temporal cron: "0 2 * * *" - Every day at 2 AM: - 1. Clone repo - 2. Analyze code (timeout 10m, retry 3x if flaky) - 3. Security scan (timeout 15m, retry 2x) - 4. Combine results - 5. Send notification - Tracks each execution - ↓ -[Results] - Full execution history - Can check status anytime -``` - ---- - -## ✨ KEY FEATURES - -| Feature | Status | Docs | Tasks | -|---------|--------|------|-------| -| One-time workflows | βœ… | ROUTING_WORKFLOW_SPEC.md | 2.1-2.4, 3.x, 4.x | -| Scheduled workflows (cron) | βœ… | CRON_JOBS_QUICK_REFERENCE.md | 2.5, 3.4, 5.x | -| Intelligent routing (LLM) | βœ… | ROUTING_WORKFLOW_SPEC.md Part 2 | 2.x | -| Smart timeouts | βœ… | ROUTING_WORKFLOW_SPEC.md | 1.2, 2.4 | -| Smart retries | βœ… | ROUTING_WORKFLOW_SPEC.md | 1.2, 2.4 | -| Error handling | βœ… | ROUTING_WORKFLOW_SPEC.md | 3.4 | -| Parameter chaining | βœ… | ROUTING_WORKFLOW_SPEC.md | 2.1 | -| Temporal durability | βœ… | ROUTING_WORKFLOW_SPEC.md | 3.4 | -| HTTP API | βœ… | ROUTING_WORKFLOW_SPEC.md | 4.1-4.4 | -| CLI | βœ… | CRON_JOBS_QUICK_REFERENCE.md | 4.2 | -| Execution tracking | βœ… | CRON_JOBS_QUICK_REFERENCE.md | 5.x | - ---- - -## πŸ“‹ QUICK START FOR IMPLEMENTATION - -### Week 1: Foundation + LLM-Router - -``` -Day 1-2 (Mon-Tue): - Task 1.1: Go types (2h) - Task 1.2: Knowledge base JSON (3h) - Task 1.3: KB loader (2h) - Task 1.4: Validator (3h) - β†’ Deliverable: Core data structures working - -Day 3-5 (Wed-Fri): - Task 2.1: JSONPath resolver (3h) - Task 2.2: Activity skeleton (2h) - Task 2.3: LLM intent analysis (5h) - Task 2.4: Spec builder (4h) - Task 2.5: Cron builder (2h) - β†’ Deliverable: llm-router generates valid specs -``` - -### Week 2: RoutingWorkflow + API/CLI - -``` -Day 1-3 (Mon-Wed): - Task 3.1-3.6: RoutingWorkflow & executors (15-18h) - Task 3.5: Register in worker - β†’ Deliverable: Workflows execute, can submit via API - -Day 4-5 (Thu-Fri): - Task 4.1: API handlers (4h) - Task 4.2: CLI commands (5h) - Task 4.3: Server bootstrap (2h) - Task 4.4: Validation (2h) - β†’ Deliverable: Full HTTP API + CLI working -``` - -### Week 3: Testing + Documentation - -``` -Day 1-3 (Mon-Wed): - Task 5.1-5.4: All tests (8-12h) - β†’ Deliverable: >90% coverage, all tests pass - -Day 4-5 (Thu-Fri): - Task 6.1-6.4: Documentation (5-8h) - β†’ Deliverable: Complete docs, ready to ship -``` - ---- - -## πŸš€ HOW TO START TODAY - -### Step 1: Read & Understand (1-2 hours) -1. Read `ROUTING_WORKFLOW_SPEC.md` (main spec) -2. Read `IMPLEMENTATION_TASKS.md` (what to build) -3. Scan `CRON_JOBS_QUICK_REFERENCE.md` (understand cron) - -### Step 2: Assign Tasks -1. Engineer 1: Tasks 1.1-1.4, 2.1-2.5, 3.1-3.6 -2. Engineer 2: Tasks 4.1-4.4, 5.1-5.4, 6.1-6.4 - -### Step 3: Start Building -1. Begin with Task 1.1 (types.go) -2. Follow dependency order -3. Daily sync on blockers - -### Step 4: Gate Each Phase -- Phase 1 done? β†’ Start Phase 2 -- Phase 2 done? β†’ Start Phase 3 -- etc. - ---- - -## πŸ“Š EFFORT SUMMARY - -| Phase | Hours | Duration | Parallel | -|-------|-------|----------|----------| -| Phase 1: Foundation | 8-10 | Mon-Tue | No | -| Phase 2: LLM-Router | 12-15 | Wed-Fri + Mon | No | -| Phase 3: RoutingWorkflow | 15-18 | Tue-Thu | Can overlap w/ Phase 4 | -| Phase 4: API/CLI | 12-15 | Fri-Tue | Can overlap w/ Phase 3 | -| Phase 5: Testing | 8-12 | Wed-Fri | Sequential | -| Phase 6: Docs | 5-8 | Fri-Mon | Parallel w/ Phase 5 | -| **TOTAL** | **60-70** | **3-4 weeks** | **2 engineers** | - ---- - -## βœ… SUCCESS CRITERIA - -**Phase 1 Complete**: -- All types compile -- Knowledge base loads -- Validator catches errors -- All unit tests pass - -**Phase 2 Complete**: -- llm-router generates valid specs -- JSONPath resolution works -- Cron detection works -- Integration tests pass - -**Phase 3 Complete**: -- RoutingWorkflow executes any spec -- Error handling works -- State machine flow correct -- Registered in worker - -**Phase 4 Complete**: -- HTTP API working (all endpoints) -- CLI working (all commands) -- Input validation -- Can submit and check status - -**Phase 5 Complete**: -- >90% code coverage -- All scenarios pass -- Performance targets met -- No flaky tests - -**Phase 6 Complete**: -- API documentation complete -- CLI documentation complete -- Deployment guide complete -- User guide with examples - ---- - -## πŸ”— FILE LOCATIONS - -``` -Core Specification: - ~/workplace/Poimen/workflows/ROUTING_WORKFLOW_SPEC.md - -Task Breakdown: - ~/workplace/Poimen/workflows/IMPLEMENTATION_TASKS.md - -Cron Reference: - ~/workplace/Poimen/workflows/CRON_JOBS_QUICK_REFERENCE.md - -Stakeholder Review: - ~/workplace/Poimen/workflows/DESIGN_MASTER_REVIEW.md - -This README: - ~/workplace/Poimen/workflows/README_IMPLEMENTATION.md -``` - ---- - -## πŸ’‘ TIPS FOR SUCCESS - -1. **Start with types** (Task 1.1) - - Everything depends on these - - Make them flexible - - Good JSON schema helps - -2. **Knowledge base is critical** (Task 1.2) - - LLM decisions are based on this - - Make it comprehensive - - Document each activity well - -3. **Test llm-router early** (Task 2.3) - - This is highest risk - - Test with real LLM calls - - Validate output quality - -4. **RoutingWorkflow is the heart** (Task 3.4) - - Make sure state machine is solid - - Test error paths thoroughly - - Performance matters - -5. **API/CLI can be simple** (Tasks 4.x) - - Just thin wrappers - - Focus on DX (developer experience) - - Good error messages - -6. **Test everything** (Phase 5) - - Unit tests catch bugs early - - Integration tests find edge cases - - E2E tests validate full flow - - Load tests validate performance - ---- - -## πŸŽ“ LEARNING RESOURCES - -- **Temporal**: https://docs.temporal.io/ -- **Cron syntax**: https://crontab.guru/ -- **JSONPath**: https://goessner.net/articles/JsonPath/ -- **Go workflow patterns**: https://golang.org/pkg/workflow - ---- - -## πŸ“ž DECISION MAKER'S CHECKLIST - -Before starting implementation: - -- [ ] Do we have LLM access? (for llm-router) -- [ ] Is Temporal deployed? (task queue "poimen-taskqueue") -- [ ] Are activities registered? (CloneRepoActivity, etc) -- [ ] Do we have memory service? (for LLM calls) -- [ ] Team aligned on architecture? -- [ ] Timeline acceptable? (3-4 weeks) -- [ ] Resources allocated? (2 engineers) - -All yes? β†’ Ready to build! πŸš€ - ---- - -**This is a complete, implementable specification.** -Start with Phase 1, Task 1.1 today! - diff --git a/REGISTERED_ACTIVITIES.md b/REGISTERED_ACTIVITIES.md deleted file mode 100644 index 2aeb95d..0000000 --- a/REGISTERED_ACTIVITIES.md +++ /dev/null @@ -1,327 +0,0 @@ -# Registered Memory Service Activities - -## Summary - -**Total Activities Registered**: 12 -**Package**: `github.com/rockliang/poimen/workflows/internal/memory` -**Registration Method**: `RegisterMemoryActivities(worker, service)` -**Task Queue**: `poimen-taskqueue` -**Namespace**: `poimen-harness` - ---- - -## Registered Activities List - -### 1. CreateKnowledgeActivity -- **Function**: `CreateKnowledgeActivity(ctx context.Context, record *KnowledgeRecord) (string, error)` -- **Input**: `KnowledgeRecord` (level, title, content, source, metadata) -- **Output**: Knowledge ID (string) -- **Timeout**: 1 minute (default) -- **Retries**: 3 attempts (default) -- **Purpose**: Create L1/L2/reference knowledge records -- **Call in Workflow**: `memory.ExecuteCreateKnowledge(ctx, record, opts)` - ---- - -### 2. UpdateKnowledgeActivity -- **Function**: `UpdateKnowledgeActivity(ctx context.Context, record *KnowledgeRecord) (string, error)` -- **Input**: `KnowledgeRecord` (with ID) -- **Output**: Knowledge ID (string) -- **Timeout**: 1 minute -- **Retries**: 3 attempts -- **Purpose**: Update existing knowledge records -- **Call in Workflow**: `memory.ExecuteUpdateKnowledge(ctx, record, opts)` (not implemented yet) - ---- - -### 3. SearchKnowledgeActivity -- **Function**: `SearchKnowledgeActivity(ctx context.Context, query string, opts *RetrievalOptions) ([]KnowledgeRecord, error)` -- **Input**: Query string + retrieval options (limit, levelFilter, floor, scope) -- **Output**: Array of `KnowledgeRecord` -- **Timeout**: 2 minutes -- **Retries**: 3 attempts -- **Purpose**: Hybrid search (semantic + lexical) -- **Call in Workflow**: `memory.ExecuteSearchKnowledge(ctx, query, opts, activityOpts)` - ---- - -### 4. GetContextActivity -- **Function**: `GetContextActivity(ctx context.Context, tool, task string, budget int) (*ServiceContext, error)` -- **Input**: Tool name, task name, budget (bytes) -- **Output**: `ServiceContext` (tier, lessons, skills, budget) -- **Timeout**: 2 minutes -- **Retries**: 3 attempts -- **Purpose**: Three-tier retrieval (signature β†’ vector β†’ reference) -- **Call in Workflow**: `memory.ExecuteGetContext(ctx, tool, task, budget, opts)` - ---- - -### 5. GetVaultActivity -- **Function**: `GetVaultActivity(ctx context.Context) ([]VaultInfo, error)` -- **Input**: None -- **Output**: Array of `VaultInfo` (path, title, level, updatedAt, recordCount) -- **Timeout**: 1 minute -- **Retries**: 3 attempts -- **Purpose**: Browse vault files and structure -- **Call in Workflow**: Use via service: `service.GetVault(ctx)` - ---- - -### 6. HealthCheckActivity -- **Function**: `HealthCheckActivity(ctx context.Context) (bool, error)` -- **Input**: None -- **Output**: Boolean (healthy or not) -- **Timeout**: 30 seconds -- **Retries**: 3 attempts -- **Purpose**: Check memory service availability -- **Call in Workflow**: `memory.ExecuteHealthCheck(ctx, opts)` - ---- - -### 7. LearnFromExecutionActivity -- **Function**: `LearnFromExecutionActivity(ctx context.Context, taskID string, result string, tags []string) (string, error)` -- **Input**: Task ID, execution result, tags (optional) -- **Output**: Knowledge record ID -- **Timeout**: 1 minute -- **Retries**: 3 attempts -- **Purpose**: Learn from task execution results -- **Call in Workflow**: `memory.ExecuteLearnFromExecution(ctx, taskID, result, tags, opts)` - ---- - -### 8. DiagnoseIssueActivity -- **Function**: `DiagnoseIssueActivity(ctx context.Context, tool, issue string) ([]string, error)` -- **Input**: Tool name, issue description -- **Output**: Array of recommendation strings -- **Timeout**: 1 minute -- **Retries**: 3 attempts -- **Purpose**: Diagnose issues using memory context -- **Call in Workflow**: `memory.ExecuteDiagnoseIssue(ctx, tool, issue, opts)` - ---- - -### 9. AnalyzeErrorActivity -- **Function**: `AnalyzeErrorActivity(ctx context.Context, errorMsg string) ([]KnowledgeRecord, error)` -- **Input**: Error message -- **Output**: Array of `KnowledgeRecord` (solutions) -- **Timeout**: 1 minute -- **Retries**: 3 attempts -- **Purpose**: Analyze errors and find recovery paths -- **Call in Workflow**: `memory.ExecuteAnalyzeError(ctx, errorMsg, opts)` - ---- - -### 10. DocumentDecisionActivity -- **Function**: `DocumentDecisionActivity(ctx context.Context, decisionType, decision, reasoning string) (string, error)` -- **Input**: Decision type, decision, reasoning -- **Output**: Knowledge record ID -- **Timeout**: 1 minute -- **Retries**: 3 attempts -- **Purpose**: Record workflow decisions (L2 knowledge) -- **Call in Workflow**: `memory.ExecuteDocumentDecision(ctx, decisionType, decision, reasoning, opts)` - ---- - -### 11. SearchAndApplyActivity -- **Function**: `SearchAndApplyActivity(ctx context.Context, query string, selector func(record *KnowledgeRecord) bool) ([]string, error)` -- **Input**: Query string, optional selector function -- **Output**: Array of applied content strings -- **Timeout**: 1 minute -- **Retries**: 3 attempts -- **Purpose**: Search knowledge and apply selective results -- **Call in Workflow**: Use via service - ---- - -### 12. RefreshMemoryActivity -- **Function**: `RefreshMemoryActivity(ctx context.Context) (map[string]interface{}, error)` -- **Input**: None -- **Output**: Map with vault stats and health -- **Timeout**: 1 minute -- **Retries**: 3 attempts -- **Purpose**: Periodic memory context refresh -- **Call in Workflow**: `memory.ExecuteRefreshMemory(ctx, opts)` - ---- - -## Registration Code - -```go -// In cmd/worker/main.go or similar -import "github.com/rockliang/poimen/workflows/internal/memory" - -func setupWorker() { - // Create memory service - memoryService := memory.NewService( - os.Getenv("MEMORY_SERVICE_URL"), - os.Getenv("MEMORY_SERVICE_TOKEN"), - "poimen", - ) - - // Register all memory activities - memory.RegisterMemoryActivities(workerInstance, memoryService) -} -``` - ---- - -## Activity Naming Convention - -Temporal activity names (as seen in logs/UI): - -``` -- CreateKnowledgeActivity β†’ createKnowledgeActivity -- UpdateKnowledgeActivity β†’ updateKnowledgeActivity -- SearchKnowledgeActivity β†’ searchKnowledgeActivity -- GetContextActivity β†’ getContextActivity -- GetVaultActivity β†’ getVaultActivity -- HealthCheckActivity β†’ healthCheckActivity -- LearnFromExecutionActivity β†’ learnFromExecutionActivity -- DiagnoseIssueActivity β†’ diagnoseIssueActivity -- AnalyzeErrorActivity β†’ analyzeErrorActivity -- DocumentDecisionActivity β†’ documentDecisionActivity -- SearchAndApplyActivity β†’ searchAndApplyActivity -- RefreshMemoryActivity β†’ refreshMemoryActivity -``` - ---- - -## Default Retry Policy - -``` -InitialInterval: 1 second -BackoffCoefficient: 2.0 -MaximumInterval: 30 seconds -MaximumAttempts: 3 -NonRetryableErrors: (empty - all errors retry) -``` - -**Timeline**: 1s β†’ 2s β†’ 4s β†’ fail - ---- - -## Default Timeouts - -| Activity | Schedule-to-Close | Start-to-Close | -|----------|-------------------|----------------| -| CreateKnowledge | 2 min | 1 min | -| SearchKnowledge | 3 min | 2 min | -| GetContext | 3 min | 2 min | -| DiagnoseIssue | 2 min | 1 min | -| AnalyzeError | 2 min | 1 min | -| LearnFromExecution | 2 min | 1 min | -| DocumentDecision | 2 min | 1 min | -| HealthCheck | 1 min | 30s | -| GetVault | 2 min | 1 min | -| RefreshMemory | 2 min | 1 min | - ---- - -## How to List Activities at Runtime - -### Option 1: Check Logs -```bash -kubectl -n poimen logs -f deployment/poimen-worker | grep "ActivityType" -``` - -### Option 2: In Workflow Test -```go -suite := &testsuite.WorkflowTestSuite{} -env := suite.NewTestActivityEnvironment() - -activities := memory.NewActivities(service) -env.RegisterActivity(activities.CreateKnowledgeActivity) -// ... etc - -// Run test - activities are registered -``` - -### Option 3: Via Temporal CLI (when connected) -```bash -temporal task-queue describe --namespace poimen-harness --task-queue poimen-taskqueue -``` - -### Option 4: Temporal Web UI -``` -http://temporal.riotpiao.com (or local Temporal UI) -β†’ Namespace: poimen-harness -β†’ Task Queue: poimen-taskqueue -β†’ View registered worker versions with activities -``` - ---- - -## Activity Flow Diagram - -``` -Workflow - ↓ -ExecuteCreateKnowledge(ctx, record, opts) - ↓ -Temporal Worker polls poimen-taskqueue - ↓ -CreateKnowledgeActivity runs with retry policy - ↓ -Memory Service HTTP call (with Bearer token) - ↓ -Result β†’ Workflow continues -``` - ---- - -## Integration with Worker - -```go -// cmd/worker/main.go - -func main() { - c, _ := client.Dial(client.Options{ - HostPort: "temporal-frontend.temporal:7233", - Namespace: "poimen-harness", - }) - defer c.Close() - - w := worker.New(c, "poimen-taskqueue", worker.Options{}) - - // Register memory activities - memSvc := memory.NewService( - "http://memory-service:8080", - os.Getenv("MEMORY_TOKEN"), - "poimen", - ) - memory.RegisterMemoryActivities(w, memSvc) - - // Start worker - w.Start() - defer w.Stop() -} -``` - ---- - -## Summary Table - -| # | Activity | Input | Output | Timeout | -|---|----------|-------|--------|---------| -| 1 | CreateKnowledge | KnowledgeRecord | string | 1m | -| 2 | UpdateKnowledge | KnowledgeRecord | string | 1m | -| 3 | SearchKnowledge | string, opts | []KnowledgeRecord | 2m | -| 4 | GetContext | tool, task, budget | ServiceContext | 2m | -| 5 | GetVault | β€” | []VaultInfo | 1m | -| 6 | HealthCheck | β€” | bool | 30s | -| 7 | LearnFromExecution | taskID, result, tags | string | 1m | -| 8 | DiagnoseIssue | tool, issue | []string | 1m | -| 9 | AnalyzeError | errorMsg | []KnowledgeRecord | 1m | -| 10 | DocumentDecision | type, decision, reason | string | 1m | -| 11 | SearchAndApply | query, selector | []string | 1m | -| 12 | RefreshMemory | β€” | map[string]interface{} | 1m | - ---- - -## Next Steps - -1. βœ… Activities defined & registered -2. βœ… All 12 activities implemented -3. πŸ”„ Deploy worker to cluster -4. πŸ”„ Verify registration in Temporal UI -5. πŸ”„ Use in workflows diff --git a/ROUTING_WORKFLOW_SPEC.md b/ROUTING_WORKFLOW_SPEC.md deleted file mode 100644 index 23a66ae..0000000 --- a/ROUTING_WORKFLOW_SPEC.md +++ /dev/null @@ -1,1198 +0,0 @@ -# Poimen Routing Workflow Specification - -**Status**: Complete Standard -**Version**: 1.0 -**Architecture**: User Message β†’ LLM-Router β†’ JSON WorkflowSpec β†’ RoutingWorkflow - ---- - -## OVERVIEW - -### The Flow - -``` -User Message: "Analyze this GitHub repo for security and quality" - ↓ -[llm-router Activity] ← LLM-powered intelligent agent - Reads: ActivityKnowledgeBase.json - ↓ - Uses LLM to understand intent: - - What is user asking for? - - Which activities are needed? - - What order? - - What timeout for each? - - What retry strategy? - ↓ - Generates JSON WorkflowSpec - { - "name": "repo-analysis", - "input": {"repo": "https://...", "branch": "main"}, - "states": [ - { - "name": "Clone", - "type": "Task", - "resource": "CloneRepoActivity", - "parameters": {"repo": "${input.repo}"}, - "timeout": "5m", ← LLM decided - "retry": {...}, ← LLM decided - "next": "Analyze" - }, - {...more states...} - ] - } - ↓ -[RoutingWorkflow] ← Generic executor - Takes the JSON spec from llm-router - Executes states in order - Respects timeout/retry - ↓ -Final Results -``` - -### Key Insight - -**Intelligence is distributed**: -- **ActivityKnowledgeBase.json**: What activities exist, their constraints, capabilities -- **llm-router Activity**: Uses LLM to reason about user intent + pick activities + decide settings -- **RoutingWorkflow**: Dumb executor that just runs what the LLM decided - ---- - -## PART 1: ACTIVITY KNOWLEDGE BASE - -### What is it? - -Metadata about all available activities. The llm-router reads this to make intelligent decisions. - -### File Location - -``` -internal/routing/activity_knowledge_base.json -``` - -### Example Format - -```json -{ - "activities": [ - { - "name": "CloneRepoActivity", - "description": "Clone a Git repository to local filesystem", - "category": "source-control", - "inputs": { - "repo": { - "type": "string", - "description": "Repository URL (https://...)", - "required": true - }, - "branch": { - "type": "string", - "description": "Branch name to clone", - "required": false, - "default": "main" - } - }, - "outputs": { - "path": { - "type": "string", - "description": "Local filesystem path to cloned repo" - }, - "commit": { - "type": "string", - "description": "Current commit hash" - } - }, - "constraints": { - "defaultTimeout": "5m", - "isFlaky": false, - "recommendedRetries": 2, - "retryBackoff": 1.5, - "dependencies": [], - "notes": "Fast operation, rarely fails" - } - }, - { - "name": "AnalyzeCodeActivity", - "description": "Analyze code for quality issues, complexity, patterns", - "category": "analysis", - "inputs": { - "path": { - "type": "string", - "description": "Path to code directory", - "required": true - }, - "language": { - "type": "string", - "description": "Programming language (go, python, js, etc)", - "required": false, - "default": "auto-detect" - } - }, - "outputs": { - "quality": { - "type": "number", - "description": "Quality score 0-1" - }, - "issues": { - "type": "array", - "description": "List of issues found" - }, - "complexity": { - "type": "number", - "description": "Cyclomatic complexity" - } - }, - "constraints": { - "defaultTimeout": "10m", - "isFlaky": true, - "recommendedRetries": 3, - "retryBackoff": 2.0, - "dependencies": ["CloneRepoActivity"], - "notes": "Can timeout on large codebases, retry recommended" - } - }, - { - "name": "SecurityScanActivity", - "description": "Scan code for vulnerabilities and security issues", - "category": "security", - "inputs": { - "path": { - "type": "string", - "description": "Path to scan", - "required": true - } - }, - "outputs": { - "vulnerabilities": { - "type": "array", - "description": "List of vulnerabilities found" - }, - "score": { - "type": "number", - "description": "Security score 0-1" - } - }, - "constraints": { - "defaultTimeout": "15m", - "isFlaky": false, - "recommendedRetries": 2, - "retryBackoff": 1.5, - "dependencies": ["CloneRepoActivity"], - "notes": "Longer runtime, can be parallelized with other scans" - } - }, - { - "name": "PerformanceAnalysisActivity", - "description": "Analyze runtime performance, bottlenecks, optimization opportunities", - "category": "analysis", - "inputs": { - "path": { - "type": "string", - "description": "Path to code", - "required": true - } - }, - "outputs": { - "performance": { - "type": "number", - "description": "Performance score 0-1" - }, - "bottlenecks": { - "type": "array", - "description": "Performance bottlenecks" - } - }, - "constraints": { - "defaultTimeout": "10m", - "isFlaky": true, - "recommendedRetries": 2, - "retryBackoff": 2.0, - "dependencies": ["CloneRepoActivity"], - "notes": "Can timeout on complex analysis" - } - }, - { - "name": "JudgeActivity", - "description": "Make final judgment/recommendation based on analysis results", - "category": "judgment", - "inputs": { - "quality": { - "type": "object", - "description": "Code quality analysis result", - "required": true - }, - "security": { - "type": "object", - "description": "Security scan result", - "required": false - }, - "performance": { - "type": "object", - "description": "Performance analysis result", - "required": false - } - }, - "outputs": { - "verdict": { - "type": "string", - "description": "approved, needs-changes, rejected" - }, - "score": { - "type": "number", - "description": "Overall score" - }, - "recommendations": { - "type": "array", - "description": "Recommendations for improvement" - } - }, - "constraints": { - "defaultTimeout": "5m", - "isFlaky": false, - "recommendedRetries": 1, - "retryBackoff": 1.0, - "dependencies": ["AnalyzeCodeActivity"], - "notes": "Fast operation, only runs after analysis" - } - }, - { - "name": "CombineResultsActivity", - "description": "Combine multiple analysis results into final report", - "category": "aggregation", - "inputs": { - "results": { - "type": "array", - "description": "Array of previous step results", - "required": true - } - }, - "outputs": { - "report": { - "type": "object", - "description": "Final combined report" - } - }, - "constraints": { - "defaultTimeout": "2m", - "isFlaky": false, - "recommendedRetries": 1, - "retryBackoff": 1.0, - "dependencies": [], - "notes": "Fast, runs at the end" - } - }, - { - "name": "SendNotificationActivity", - "description": "Send notification with results to user", - "category": "notification", - "inputs": { - "result": { - "type": "object", - "description": "Results to send", - "required": true - }, - "channel": { - "type": "string", - "description": "Where to send (email, slack, etc)", - "required": false, - "default": "email" - } - }, - "outputs": { - "sent": { - "type": "boolean", - "description": "Was notification sent?" - } - }, - "constraints": { - "defaultTimeout": "2m", - "isFlaky": true, - "recommendedRetries": 3, - "retryBackoff": 2.0, - "dependencies": [], - "notes": "External dependency, retry recommended" - } - } - ] -} -``` - -### Knowledge Base Fields Explained - -| Field | Purpose | Used By | -|-------|---------|---------| -| **name** | Unique activity identifier | llm-router for routing | -| **description** | What the activity does | llm-router for reasoning | -| **category** | Type of activity (analysis, security, etc) | llm-router for grouping | -| **inputs** | What the activity accepts | llm-router to chain activities | -| **outputs** | What the activity produces | llm-router to connect outputs to next activity inputs | -| **constraints.defaultTimeout** | Recommended timeout | llm-router to decide timeout | -| **constraints.isFlaky** | Does it fail often? | llm-router to decide retry count | -| **constraints.recommendedRetries** | How many retries? | llm-router to configure retry | -| **constraints.retryBackoff** | Backoff multiplier | llm-router to configure exponential backoff | -| **constraints.dependencies** | Must run after X | llm-router to order activities | -| **constraints.notes** | Special handling notes | llm-router for reasoning | - ---- - -## PART 2: LLM-ROUTER ACTIVITY - -### What is it? - -An Activity that uses an LLM to intelligently decide: -1. Which activities are needed -2. What order to execute them -3. What timeout for each activity -4. What retry policy for each activity -5. How to chain parameters (JSONPath) - -### Input to llm-router - -```json -{ - "message": "Analyze this GitHub repo for security and code quality", - "repo": "https://github.com/rockliang/poimen", - "branch": "main" -} -``` - -### Output from llm-router - -```json -{ - "name": "repo-analysis-workflow", - "input": { - "repo": "https://github.com/rockliang/poimen", - "branch": "main" - }, - "states": [ - { - "name": "Clone", - "type": "Task", - "resource": "CloneRepoActivity", - "parameters": { - "repo": "${input.repo}", - "branch": "${input.branch}" - }, - "timeout": "5m", - "retry": { - "maxAttempts": 2, - "backoffRate": 1.5, - "initialInterval": "1s" - }, - "next": "Analyze" - }, - { - "name": "Analyze", - "type": "Task", - "resource": "AnalyzeCodeActivity", - "parameters": { - "path": "${Clone.output.path}" - }, - "timeout": "10m", - "retry": { - "maxAttempts": 3, - "backoffRate": 2.0, - "initialInterval": "1s" - }, - "catch": [ - { - "errorEquals": ["Timeout"], - "next": "HandleTimeout" - } - ], - "next": "SecurityScan" - }, - { - "name": "SecurityScan", - "type": "Task", - "resource": "SecurityScanActivity", - "parameters": { - "path": "${Clone.output.path}" - }, - "timeout": "15m", - "retry": { - "maxAttempts": 2, - "backoffRate": 1.5, - "initialInterval": "1s" - }, - "next": "Combine" - }, - { - "name": "Combine", - "type": "Task", - "resource": "CombineResultsActivity", - "parameters": { - "results": [ - "${Analyze.output}", - "${SecurityScan.output}" - ] - }, - "timeout": "2m", - "retry": { - "maxAttempts": 1, - "backoffRate": 1.0, - "initialInterval": "1s" - }, - "next": "SendResults" - }, - { - "name": "SendResults", - "type": "Task", - "resource": "SendNotificationActivity", - "parameters": { - "result": "${Combine.output.report}", - "channel": "email" - }, - "timeout": "2m", - "retry": { - "maxAttempts": 3, - "backoffRate": 2.0, - "initialInterval": "1s" - }, - "end": true - }, - { - "name": "HandleTimeout", - "type": "Fail", - "error": "AnalysisTimeout", - "cause": "Code analysis timed out after 10 minutes" - } - ] -} -``` - -### LLM-Router Logic (Pseudo-code) - -```python -def llm_router_activity(message: str, repo: str, branch: str) -> WorkflowSpec: - # 1. Load knowledge base - knowledge_base = load_activity_knowledge_base() - - # 2. Use LLM to understand intent - intent = llm.analyze_intent(message) - # intent = { - # "needs_security_scan": true, - # "needs_code_analysis": true, - # "needs_performance_analysis": false, - # "needs_final_judgment": true, - # } - - # 3. Select activities based on intent - selected_activities = [] - if intent.needs_security_scan: - selected_activities.append(knowledge_base["SecurityScanActivity"]) - if intent.needs_code_analysis: - selected_activities.append(knowledge_base["AnalyzeCodeActivity"]) - if intent.needs_performance_analysis: - selected_activities.append(knowledge_base["PerformanceAnalysisActivity"]) - - # 4. Order activities by dependencies - ordered = topological_sort_by_dependencies(selected_activities) - - # 5. Build workflow states with intelligent settings - states = [] - - # Always start with Clone - clone_state = { - "name": "Clone", - "type": "Task", - "resource": "CloneRepoActivity", - "parameters": { - "repo": "${input.repo}", - "branch": "${input.branch}" - }, - "timeout": "5m", # From knowledge base - "retry": { - "maxAttempts": 2, - "backoffRate": 1.5, - "initialInterval": "1s" - }, - "next": ordered[0]["name"] - } - states.append(clone_state) - - # Add selected activities in order - for i, activity in enumerate(ordered): - state = { - "name": activity["name"].replace("Activity", ""), - "type": "Task", - "resource": activity["name"], - "parameters": { - # Use LLM to map inputs - # For each input, decide: use output from previous step? use input param? use constant? - }, - "timeout": activity["constraints"]["defaultTimeout"], - "retry": { - "maxAttempts": activity["constraints"]["recommendedRetries"], - "backoffRate": activity["constraints"]["retryBackoff"], - "initialInterval": "1s" - }, - "next": ordered[i+1]["name"] if i+1 < len(ordered) else "SendResults" - } - - # Add error handling if flaky - if activity["constraints"]["isFlaky"]: - state["catch"] = [ - { - "errorEquals": ["Timeout"], - "next": "HandleTimeout" - } - ] - - states.append(state) - - # Add final notification - states.append({ - "name": "SendResults", - "type": "Task", - "resource": "SendNotificationActivity", - "parameters": { - "result": "${" + ordered[-1]["name"].replace("Activity", "") + ".output}", - "channel": "email" - }, - "timeout": "2m", - "retry": {"maxAttempts": 3, "backoffRate": 2.0, "initialInterval": "1s"}, - "end": true - }) - - # Add error handler - states.append({ - "name": "HandleTimeout", - "type": "Fail", - "error": "AnalysisTimeout", - "cause": "Activity timed out" - }) - - # 6. Return complete workflow spec - return WorkflowSpec( - name="generated-workflow", - input={"repo": repo, "branch": branch}, - states=states - ) -``` - -### Go Implementation Skeleton - -```go -// cmd/worker/activities/llm_router.go - -type LLMRouterInput struct { - Message string - Repo string - Branch string -} - -func LLMRouterActivity(ctx context.Context, input LLMRouterInput) (routing.WorkflowSpec, error) { - // 1. Load activity knowledge base - kb, err := LoadActivityKnowledgeBase() - if err != nil { - return routing.WorkflowSpec{}, err - } - - // 2. Use LLM to understand intent - // Call memory service to get LLM analysis - intent, err := llm.AnalyzeIntent(ctx, input.Message, kb) - if err != nil { - return routing.WorkflowSpec{}, err - } - - // 3. Build workflow spec intelligently - spec := buildWorkflowSpec(intent, input, kb) - - return spec, nil -} - -func buildWorkflowSpec(intent Intent, input LLMRouterInput, kb ActivityKnowledgeBase) routing.WorkflowSpec { - // Create workflow with intelligent settings from knowledge base - // ... -} -``` - ---- - -## PART 3: ROUTING WORKFLOW (The Executor) - -### What is it? - -A generic Temporal workflow that executes ANY WorkflowSpec generated by llm-router. - -### Input - -```go -type RoutingWorkflowInput struct { - WorkflowSpec routing.WorkflowSpec // Generated by llm-router -} -``` - -### Implementation - -```go -func RoutingWorkflow(ctx workflow.Context, spec routing.WorkflowSpec) (Result, error) { - logger := workflow.GetLogger(ctx) - - // Initialize context - ec := &ExecutionContext{ - Input: spec.Input, - StepResults: make(map[string]interface{}), - } - - // Find first state - currentStateName := FindFirstState(spec.States) - - // State machine loop - for { - logger.Info("Executing state", "state", currentStateName) - - state := FindStateByName(spec.States, currentStateName) - if state == nil { - return Result{}, fmt.Errorf("state not found: %s", currentStateName) - } - - // Execute state (Task, Pass, Fail, etc) - result, err := ExecuteState(ctx, state, ec) - - // Error handling - if err != nil { - for _, catchClause := range state.Catch { - if MatchesError(err, catchClause.ErrorEquals) { - logger.Info("Error caught", "handler", catchClause.Next) - currentStateName = catchClause.Next - break - } - } - return Result{}, fmt.Errorf("state %s failed: %w", currentStateName, err) - } - - // Store result - ec.StepResults[state.Name] = result - - // Check if done - if state.End { - logger.Info("Workflow completed") - return Result{ - FinalOutput: result, - Status: "COMPLETED", - }, nil - } - - // Move to next - currentStateName = state.Next - } -} -``` - ---- - -## PART 4: WORKFLOW SPEC FORMAT - -### State Types - -The llm-router generates states with these types: - -#### Task (Execute Activity) - -```json -{ - "name": "Clone", - "type": "Task", - "resource": "CloneRepoActivity", - "parameters": { - "repo": "${input.repo}", - "branch": "${input.branch:main}" - }, - "timeout": "5m", - "retry": { - "maxAttempts": 3, - "backoffRate": 2.0, - "initialInterval": "1s" - }, - "catch": [ - { - "errorEquals": ["Timeout", "NetworkError"], - "next": "HandleError" - } - ], - "next": "Analyze" -} -``` - -**LLM decides**: -- Which activity to use -- How to chain parameters (${Clone.output.path}) -- Timeout (from knowledge base + LLM reasoning) -- Retry policy (from knowledge base + isFlaky flag) -- Error handling (catch blocks for flaky activities) - -#### Pass (Static Output) - -```json -{ - "name": "CombineResults", - "type": "Pass", - "result": { - "status": "success", - "report": "Analysis complete" - }, - "next": "SendResults" -} -``` - -#### Fail (Terminate) - -```json -{ - "name": "HandleError", - "type": "Fail", - "error": "AnalysisFailed", - "cause": "Security scan timed out" -} -``` - ---- - -## PART 5: EXECUTION FLOW EXAMPLE - -### User Request - -``` -"Analyze the GitHub repo rockliang/poimen for security vulnerabilities and code quality" -``` - -### Step 1: llm-router Activity - -``` -Input: {message: "...", repo: "https://github.com/rockliang/poimen", branch: "main"} - -LLM Analysis: - - User wants: security analysis + code quality - - Sequence: Clone β†’ Analyze (quality) β†’ SecurityScan β†’ CombineResults β†’ Notify - - Timeouts: Use defaults from knowledge base - - Clone: 5m (not flaky) - - Analyze: 10m (flaky, so 3 retries) - - SecurityScan: 15m (not flaky, so 2 retries) - - Combine: 2m (not flaky, 1 retry) - - Error handling: Add catch blocks for flaky activities (Analyze) - -Output: WorkflowSpec (as JSON above) -``` - -### Step 2: RoutingWorkflow Executes - -``` -State 1: Clone - Execute: CloneRepoActivity({repo: "https://...", branch: "main"}) - Timeout: 5m (from llm decision) - Result: {path: "/tmp/cloned-repo"} - ↓ -State 2: Analyze - Execute: AnalyzeCodeActivity({path: "/tmp/cloned-repo"}) - Timeout: 10m - Retry: 3 attempts (from llm decision, isFlaky=true) - Attempt 1: TIMEOUT β†’ Wait 1s - Attempt 2: TIMEOUT β†’ Wait 2s - Attempt 3: SUCCESS β†’ {quality: 0.85, issues: [...]} - ↓ -State 3: SecurityScan - Execute: SecurityScanActivity({path: "/tmp/cloned-repo"}) - Timeout: 15m - Retry: 2 attempts (from llm decision, isFlaky=false) - Result: {vulnerabilities: [], score: 0.95} - ↓ -State 4: CombineResults - Execute: CombineResultsActivity({results: [...]}) - Result: {report: {...}} - ↓ -State 5: SendResults - Execute: SendNotificationActivity({result: {...}, channel: "email"}) - Result: {sent: true} - ↓ -βœ… WORKFLOW COMPLETED -``` - ---- - -## PART 6: GO TYPE DEFINITIONS - -```go -package routing - -import "time" - -// WorkflowSpec is generated by llm-router (one-time execution) -type WorkflowSpec struct { - Name string `json:"name"` - Input map[string]interface{} `json:"input"` - States []State `json:"states"` -} - -// CronWorkflowSpec is generated by llm-router (scheduled execution) -type CronWorkflowSpec struct { - Name string `json:"name"` - Type string `json:"type"` // "CronWorkflow" - Schedule string `json:"schedule"` // Cron expression (e.g., "0 2 * * *") - Timezone string `json:"timezone"` // "UTC", "America/New_York", etc - Input map[string]interface{} `json:"input"` // Fixed input for each run - States []State `json:"states"` // Workflow states - MaxConcurrent int `json:"maxConcurrent,omitempty"` // Max parallel runs (default 1) - Timeout string `json:"timeout,omitempty"` // Overall timeout per run - EnableHistory bool `json:"enableHistory,omitempty"` // Keep execution history -} - -// State is a step in the workflow -type State struct { - Name string - Type StateType // "Task", "Pass", "Fail" - - // Task fields - Resource string - Parameters map[string]interface{} - Timeout string - Retry *RetryPolicy - Catch []CatchClause - - // Pass fields - Result interface{} - - // Fail fields - Error string - Cause string - - // Transition - Next string - End bool -} - -type StateType string - -const ( - StateTypeTask StateType = "Task" - StateTypePass StateType = "Pass" - StateTypeFail StateType = "Fail" -) - -// RetryPolicy (LLM decides this from knowledge base) -type RetryPolicy struct { - MaxAttempts int32 `json:"maxAttempts"` - BackoffRate float64 `json:"backoffRate"` - InitialInterval string `json:"initialInterval"` - MaxInterval string `json:"maxInterval,omitempty"` -} - -// CatchClause for error handling -type CatchClause struct { - ErrorEquals []string `json:"errorEquals"` - Next string `json:"next"` -} - -// ExecutionContext tracks state -type ExecutionContext struct { - Input map[string]interface{} - StepResults map[string]interface{} -} - -// Result is final output -type Result struct { - FinalOutput interface{} - Status string - Error error -} -``` - ---- - -## PART 7: CRON JOB SCHEDULING - -### What is it? - -llm-router can generate workflows that execute **on a schedule** (cron jobs), not just one-time. - -### Example: Daily Security Scan - -User: "Scan all repositories for security vulnerabilities every day at 2 AM" - -llm-router generates: - -```json -{ - "name": "daily-security-scan", - "type": "CronWorkflow", - "schedule": "0 2 * * *", - "timezone": "UTC", - "input": { - "repos": [ - "https://github.com/rockliang/poimen", - "https://github.com/rockliang/workflow-engine" - ] - }, - "states": [ - { - "name": "Clone", - "type": "Task", - "resource": "CloneRepoActivity", - "parameters": { - "repo": "${input.repos[0]}" - }, - "timeout": "5m", - "retry": {"maxAttempts": 2, "backoffRate": 1.5, "initialInterval": "1s"}, - "next": "SecurityScan" - }, - { - "name": "SecurityScan", - "type": "Task", - "resource": "SecurityScanActivity", - "parameters": {"path": "${Clone.output.path}"}, - "timeout": "15m", - "retry": {"maxAttempts": 2, "backoffRate": 1.5, "initialInterval": "1s"}, - "next": "SendAlert" - }, - { - "name": "SendAlert", - "type": "Task", - "resource": "SendNotificationActivity", - "parameters": { - "result": "${SecurityScan.output}", - "channel": "slack" - }, - "timeout": "2m", - "retry": {"maxAttempts": 3, "backoffRate": 2.0, "initialInterval": "1s"}, - "end": true - } - ] -} -``` - -### Cron Format - -Standard Unix cron syntax: - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ minute (0 - 59) -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ hour (0 - 23) -β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ day of month (1 - 31) -β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ month (1 - 12) -β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ day of week (0 - 6, 0 = Sunday) -β”‚ β”‚ β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ β”‚ β”‚ -* * * * * - -Examples: -0 2 * * * = Every day at 2:00 AM -0 */6 * * * = Every 6 hours -0 9 * * 1-5 = Weekdays at 9 AM -0 0 1 * * = First day of month at midnight -30 2 * * 0 = Every Sunday at 2:30 AM -*/15 * * * * = Every 15 minutes -0 12 * * * = Every day at noon -``` - -### Cron Workflow Fields - -```go -type CronWorkflowSpec struct { - Name string `json:"name"` - Type string `json:"type"` // "CronWorkflow" - Schedule string `json:"schedule"` // Cron expression - Timezone string `json:"timezone"` // "UTC", "America/New_York", etc - Input map[string]interface{} `json:"input"` // Fixed input for each run - States []State `json:"states"` // Same as regular workflow - - // Optional - MaxConcurrent int `json:"maxConcurrent,omitempty"` // Max parallel executions (default 1) - Timeout string `json:"timeout,omitempty"` // Overall timeout per run (default 1h) - EnableHistory bool `json:"enableHistory,omitempty"` // Keep execution history? -} -``` - -### Temporal Cron Job Implementation - -```go -func SubmitCronWorkflow(cronSpec CronWorkflowSpec) error { - client, _ := client.Dial(client.ClientOptions{ - HostPort: "temporal-frontend.temporal:7233", - }) - - // For cron, we still use RoutingWorkflow - // But Temporal handles the scheduling - - _, err := client.ExecuteWorkflow( - context.Background(), - client.StartWorkflowOptions{ - ID: cronSpec.Name, - TaskQueue: "poimen-taskqueue", - CronSchedule: cronSpec.Schedule, // ← Temporal cron support - }, - RoutingWorkflow, - cronSpec.States, - ) - - return err -} -``` - -### Example Use Cases - -``` -1. Daily Security Scan (0 2 * * *) - Input: {repos: [...]} - Runs: Every day at 2 AM - ↓ - Clone all repos β†’ Security scan β†’ Send alert - -2. Hourly Code Quality Check (0 * * * *) - Input: {repo: "https://github.com/rockliang/poimen"} - Runs: Every hour - ↓ - Clone β†’ Analyze β†’ Judge β†’ Log results - -3. Weekly Performance Benchmark (0 3 * * 0) - Input: {repos: [...], benchmark: true} - Runs: Every Sunday at 3 AM - ↓ - Clone β†’ Performance test β†’ Compare with baseline β†’ Report - -4. Every 15 minutes Health Check (*/15 * * * *) - Input: {service: "api"} - Runs: Every 15 minutes - ↓ - Check service health β†’ Send metrics β†’ Alert if down -``` - -### llm-router for Cron Jobs - -User: "Run daily security scan at 2 AM UTC on all our repositories" - -llm-router decision: - -```python -def llm_router_activity(message: str) -> WorkflowSpec: - intent = llm.analyze_intent(message) - # intent = { - # "is_scheduled": True, - # "schedule": "0 2 * * *", # LLM extracts from "2 AM" - # "timezone": "UTC", - # "needs_security_scan": True, - # "target": "all repositories" - # } - - if intent.is_scheduled: - return CronWorkflowSpec( - name="daily-security-scan", - type="CronWorkflow", - schedule=intent.schedule, # "0 2 * * *" - timezone=intent.timezone, # "UTC" - input={"repos": get_all_repos()}, - states=[ - # Same states as one-time workflow - ] - ) - else: - return OneTimeWorkflowSpec(...) -``` - -### Execution History - -When `enableHistory=true`, Temporal tracks all cron executions: - -``` -Cron Workflow ID: daily-security-scan - -Execution 1: 2025-02-01 02:00:00 UTC β†’ COMPLETED -Execution 2: 2025-02-02 02:00:00 UTC β†’ COMPLETED -Execution 3: 2025-02-03 02:00:00 UTC β†’ FAILED (timeout) -Execution 4: 2025-02-04 02:00:00 UTC β†’ COMPLETED -Execution 5: 2025-02-05 02:00:00 UTC β†’ COMPLETED (in progress...) -``` - ---- - -## PART 8: VALIDATION - -Before RoutingWorkflow executes the spec, validate: - -``` -βœ… All state names are unique -βœ… All state.next references exist -βœ… All catch.next references exist -βœ… All Task states have resource defined -βœ… No circular loops (Aβ†’Bβ†’Cβ†’A) -βœ… At least one path leads to end -βœ… No dead ends -βœ… Timeout format is valid (e.g., "5m") -βœ… Retry settings are valid -βœ… JSONPath expressions are syntactically valid -``` - ---- - -## SUMMARY - -### The Intelligence is in llm-router - -**llm-router activity**: -- βœ… Reads ActivityKnowledgeBase.json -- βœ… Uses LLM to understand user intent -- βœ… Intelligently selects activities to run -- βœ… Orders them by dependencies -- βœ… Decides timeout for EACH activity (from knowledge base + LLM reasoning) -- βœ… Decides retry policy for EACH activity (flaky? retry 3x : 1x) -- βœ… Chains parameters using JSONPath (${Clone.output.path}) -- βœ… Adds error handling (catch blocks) -- βœ… **Detects scheduled workflows** (cron jobs) and generates CronWorkflowSpec -- βœ… Returns complete JSON WorkflowSpec (one-time or recurring) - -### The Executor is Generic - -**RoutingWorkflow**: -- βœ… Takes JSON spec from llm-router -- βœ… Executes states in order -- βœ… Respects timeout/retry -- βœ… Handles errors with catch blocks -- βœ… Works for both one-time and cron workflows -- βœ… Returns results - -### Execution Modes - -| Mode | Triggered By | Execution | Example | -|------|---|---|---| -| **One-Time** | API call / CLI / Direct | Runs once, returns immediately | Analyze repo, generate report | -| **Cron Job** | Schedule | Runs repeatedly on schedule | Daily security scan at 2 AM | -| **Blocking** | User waits | Synchronous, returns results | Execute template, get output | -| **Async** | User polls | Asynchronous, check status later | Submit workflow, poll status | - -### Files Needed - -``` -1. internal/routing/activity_knowledge_base.json - ↑ Static metadata about activities (timeouts, retry, flaky, etc) - -2. cmd/worker/activities/llm_router.go - ↑ LLM-powered activity that generates WorkflowSpec or CronWorkflowSpec - -3. statemachine/routing_workflow.go - ↑ Generic executor for any spec (one-time or cron) - -4. internal/routing/validator.go - ↑ Validate specs before execution - ↑ Validate cron expressions - -5. cmd/api-server/cron_handlers.go (optional) - ↑ Endpoints to manage cron workflows - ↑ GET /api/v1/cron/{id}/status - ↑ DELETE /api/v1/cron/{id} (cancel) -``` - -This is the **correct, complete architecture**! 🎯 - -### Quick Feature Checklist - -- βœ… **One-time workflows** (API/CLI submit) -- βœ… **Scheduled workflows** (Cron jobs with schedule) -- βœ… **Intelligent routing** (LLM decides what to run) -- βœ… **Smart timeouts** (From activity knowledge base) -- βœ… **Smart retries** (Based on isFlaky flag) -- βœ… **Error handling** (Catch blocks for flaky activities) -- βœ… **Parameter chaining** (JSONPath resolution) -- βœ… **Execution history** (For cron jobs) -- βœ… **Temporal durability** (Automatic replay) - diff --git a/TEMPORAL_USAGE.md b/TEMPORAL_USAGE.md deleted file mode 100644 index b65247a..0000000 --- a/TEMPORAL_USAGE.md +++ /dev/null @@ -1,269 +0,0 @@ -# Temporal Integration for Poimen Workflows - -## Overview - -This project uses **Temporal** for distributed workflow orchestration. Instead of connecting directly to Temporal ports, we use the **REST API Gateway** at `https://api.riotpiao.com/workflow`. - -**Reference Documentation**: See `~/workplace/homelab-frontend/TEMPORAL_USAGE.md` for full API details. - ---- - -## Quick Start - -### Configuration - -The Temporal connection is configured via environment variables: - -```bash -TEMPORAL_NAMESPACE=poimen-harness # Default namespace -TEMPORAL_HOSTPORT=api.riotpiao.com/workflow # REST API gateway (CI only) -# Direct gRPC in K8s: -TEMPORAL_HOSTPORT=temporal-frontend.temporal:7233 # K8s DNS -``` - -### For CI/CD (Proper Authentication via PAT Token) - -The CI runner uses a PAT (Personal Access Token) for Forgejo authentication. Integration tests gracefully handle Temporal availability: - -1. **Git authentication configured** in CI: - - `.gitea/workflows/ci.yaml` uses `${{ secrets.REGISTRY_PAT }}` token - - Enables private module access and authenticated requests - -2. **Integration tests behavior**: - ```bash - go test -v ./... # Runs all tests - ``` - - If Temporal accessible: βœ… Tests run - - If Temporal unavailable: ⏭️ Tests skip gracefully - -3. **Local development** (with Temporal access): - ```bash - go test -v -run TestTemporal ./tests - ``` - -4. **Graceful fallback**: - ```go - // tests/temporal_integration_test.go - if err != nil { - t.Skipf("skipping: Temporal not accessible - %v", err) - } - ``` - ---- - -## Rest API Gateway Usage - -### Base URL -``` -https://api.riotpiao.com/workflow -``` - -### Example: Start a Workflow (from CI) - -Instead of: -```go -// ❌ This fails in CI (no direct access) -c, err := client.Dial(client.Options{ - HostPort: "127.0.0.1:7233", - Namespace: "poimen-harness", -}) -``` - -Use HTTP REST calls: -```bash -curl -X POST https://api.riotpiao.com/workflow \ - -H 'Content-Type: application/json' \ - -d '{ - "action": "START_WORKFLOW", - "namespace": "poimen-harness", - "payload": { - "workflow_id": "test-workflow", - "workflow_type": "OrchestratorWorkflow", - "task_queue": "poimen-taskqueue", - "input": {} - } - }' -``` - -### Operations Available - -All standard Temporal operations: -- `START_WORKFLOW` - Launch new workflow -- `DESCRIBE_WORKFLOW` - Get workflow status -- `LIST_WORKFLOWS` - List executions -- `GET_WORKFLOW_HISTORY` - View event history -- `SIGNAL_WORKFLOW` - Send signals to running workflows -- `QUERY_WORKFLOW` - Query workflow state -- `TERMINATE_WORKFLOW` - Stop workflow -- `CANCEL_WORKFLOW` - Graceful cancellation - -See `~/workplace/homelab-frontend/TEMPORAL_USAGE.md` for full operation reference. - ---- - -## Project Structure - -``` -. -β”œβ”€β”€ cmd/ -β”‚ β”œβ”€β”€ starter/ - CLI to start workflows (requires Temporal access) -β”‚ └── worker/ - Worker that processes tasks -β”œβ”€β”€ tests/ -β”‚ β”œβ”€β”€ git_test.go - Unit tests (run in CI βœ…) -β”‚ β”œβ”€β”€ types_test.go - Unit tests (run in CI βœ…) -β”‚ └── temporal_integration_test.go - Integration tests (skipped in CI, local only) -β”œβ”€β”€ statemachine/ -β”‚ β”œβ”€β”€ orchestrator.go - Main workflow definition -β”‚ └── taskunit.go - Sub-workflow for tasks -└── action/ - β”œβ”€β”€ git.go - Git operations (activities) - β”œβ”€β”€ planner.go - Planning activity - β”œβ”€β”€ implementer.go - Implementation activity - └── judge.go - Judgment activity -``` - ---- - -## Running Tests - -### Unit Tests (CI Compatible) -```bash -go test -v ./tests # βœ… Passes in CI -``` - -### Integration Tests (Local Only) -```bash -# Requires TEMPORAL_HOSTPORT to point to accessible Temporal -go test -v -run TestTemporal ./tests - -# Or in K8s environment: -kubectl exec -it deployment/poimen-worker -- \ - go test -v ./tests -``` - ---- - -## Worker Deployment - -### Local Development -```bash -# Start worker (requires Temporal access) -TEMPORAL_HOSTPORT=localhost:7233 go run ./cmd/worker -``` - -### Kubernetes -```bash -kubectl apply -k k8s/ -# Workers connect to temporal-frontend.temporal:7233 (K8s DNS) -``` - -### Configuration -See `k8s/configmap.yaml`: -```yaml -TEMPORAL_NAMESPACE: "poimen-harness" -TEMPORAL_HOSTPORT: "temporal-frontend.temporal:7233" -``` - ---- - -## CI/CD Pipeline - -The `.gitea/workflows/ci.yaml` runs: - -1. **Git Auth** - Configure Forgejo PAT token for authentication -2. **Checkout** - Pull code -3. **Dependencies** - `go mod download` -4. **Tests** - `go test -v ./...` - - Unit tests: βœ… Always pass - - Integration tests: βœ… Run if Temporal accessible, ⏭️ skip if not -5. **Build** - `go build ./cmd/...` -6. **Vet** - `go vet ./...` - -βœ… **Always passes** - Proper authentication + graceful test fallback - ---- - -## Accessing the Temporal UI - -### Web UI -``` -https://api.riotpiao.com (UI frontend) -``` - -### Metrics -```bash -curl https://api.riotpiao.com/workflow/metrics -``` - -### Health Check -```bash -curl https://api.riotpiao.com/workflow/health -``` - ---- - -## Environment Variables Reference - -| Variable | Default | Usage | CI | -|----------|---------|-------|----| -| `TEMPORAL_NAMESPACE` | `poimen-harness` | Workflow namespace | βœ… | -| `TEMPORAL_HOSTPORT` | `localhost:7233` | Server address | βœ… (configurable) | -| `ANTHROPIC_API_KEY` | (required) | LLM for AI agents | βœ… (secret) | -| `GOPRIVATE` | (empty) | Private module auth | βœ… | -| `REGISTRY_PAT` | (required) | Forgejo auth token | βœ… (secret) | - ---- - -## Troubleshooting - -### "connection refused" in CI -βœ… **Expected & OK** - Integration tests gracefully skip if Temporal unavailable -```bash -# Check: integration tests handle connection errors -go test -v ./tests -# Output: SKIP temporal_integration_test.go:32 (Temporal not accessible) -``` - -### Tests fail locally with "connection refused" -Ensure Temporal is accessible: -```bash -# Check connectivity -curl https://api.riotpiao.com/workflow/health - -# Or for local Temporal: -nc -zv localhost 7233 -``` - -### Worker can't reach Temporal in K8s -Verify: -```bash -# Check configmap -kubectl get cm poimen-config -o yaml - -# Check pod logs -kubectl logs deployment/poimen-worker - -# Verify DNS from pod -kubectl exec -it deployment/poimen-worker -- \ - nslookup temporal-frontend.temporal -``` - ---- - -## Next Steps - -1. βœ… CI tests pass with proper authentication (PAT token) -2. βœ… Integration tests run when Temporal accessible, skip otherwise -3. πŸ”„ Local development: access Temporal for full integration test coverage -4. πŸ“¦ K8s deployment: workers connect to Temporal service -5. πŸ“Š Monitor via REST API: `https://api.riotpiao.com/workflow` - ---- - -## References - -- **Full API**: `~/workplace/homelab-frontend/TEMPORAL_USAGE.md` -- **K8s Config**: `./k8s/configmap.yaml` -- **CI Config**: `.gitea/workflows/ci.yaml` -- **Worker Code**: `./cmd/worker/main.go` -- **Workflows**: `./statemachine/orchestrator.go` diff --git a/TOOL_USAGE_AND_SKILLS.md b/TOOL_USAGE_AND_SKILLS.md deleted file mode 100644 index 9a320f0..0000000 --- a/TOOL_USAGE_AND_SKILLS.md +++ /dev/null @@ -1,585 +0,0 @@ -# Tool Usage & Skills Ingestion Strategy - -## Poimen Tool Landscape - -### Category 1: Workflow Definition Tools - -**Tool**: `WorkflowDef Builder` (Rust) -```rust -let workflow = WorkflowDef::builder() - .name("poimen") - .phase(T0::phases())? - .step(StepId::from("T0.1-identity"))? - .transition_to(StepId::from("T0.2-kernel"))? - .build()?; -``` - -**Skill Usage**: -- Know when to use builder vs YAML -- Understand phase dependencies -- Handle schema version mismatches - -**Memory Integration**: -``` -IngestActivity { - level: "L2", - title: "WorkflowDef Builder Pattern", - content: "Use builder for Rust workflows. YAML for runtime customization.", - tags: ["T3-canonicalization", "IR"], -} -``` - ---- - -### Category 2: State Machine Tools - -**Tool**: `Event Log` (immutable JSONL) -``` -{"attempt_id": "1", "step": "T0.1", "event": "WorkerEvent::Started"} -{"attempt_id": "1", "step": "T0.1", "event": "WorkerEvent::Completed"} -{"attempt_id": "1", "step": "T0.2", "event": "WorkerEvent::Attempted"} -``` - -**Skills**: -- Event log format and ordering -- Atomic commit protocol for writes -- Fold + re-derive pattern - -**Memory Integration**: -``` -SearchActivity { - query: "event log corruption recovery", - returns: ["Verify checksum", "Replay from marker", "Fork + rewind"] -} -``` - -**Tool**: `Fold & Re-derive` -```rust -fn fold_state(state: &mut AttemptState, event: &WorkerEvent) { - match event { - WorkerEvent::Started => state.status = Running, - WorkerEvent::Completed => state.status = Success, - // ... - } -} -``` - -**Skills**: -- Deterministic state transitions -- No side effects in fold -- Time-ordered replay - -**Memory Integration**: -``` -DiagnoseIssueActivity { - issue: "state divergence after event log replay", - returns: [ - "Tier 1: Check for non-deterministic fold", - "Tier 2: Verify event order", - "Tier 3: See fold/re-derive docs" - ] -} -``` - ---- - -### Category 3: Execution Tools - -**Tool**: `Run Executor` (polling) -```rust -loop { - let task = queue.wait_for_task(timeout)?; - let output = executor.execute_step(&task)?; - queue.mark_complete(&task, &output)?; -} -``` - -**Skills**: -- Long-poll timeouts -- Task queue semantics -- Backpressure handling - -**Memory Integration**: -``` -IngestActivity { - level: "L1", - title: "Executor Timeout Pattern", - content: "20s task queue poll, 30s step timeout, exponential backoff", - tags: ["executor", "T1-execution"], -} -``` - -**Tool**: `Attempt Lifecycle` -```rust -pub struct AttemptState { - number: u32, // 1st, 2nd, 3rd attempt - started_at: SystemTime, - budget: Budget, // tokens, attempts, time - context: PartitionedContext, // input for this attempt - retry_policy: RetryPolicy, -} -``` - -**Skills**: -- Budget exhaustion detection -- Retry condition evaluation -- Context capture per attempt - -**Memory Integration**: -``` -ContextActivity { - tool: "executor", - task: "attempt-lifecycle", - returns: { - tier_1: "Known budget limits per phase", - tier_2: "Learned attempt success rates", - tier_3: "Docs on RetryPolicy tuning", - } -} -``` - ---- - -### Category 4: Verification Tools - -**Tool**: `Verifier Port` (pluggable) -```rust -pub trait Verifier { - fn verify(&self, output: &Output, rubric: &Rubric) -> Result; -} -``` - -**Skills**: -- Rubric definition (JSON/YAML) -- Verification logic chains -- Failure categorization - -**Memory Integration**: -``` -SearchActivity { - query: "rubric evaluation patterns", - returns: [ - "Multi-level rubric structure", - "Failure classification system", - "Score aggregation methods" - ] -} -``` - -**Tool**: `Judge Port` (decision logic) -```rust -pub trait Judge { - fn decide(&self, attempt: &AttemptState) -> Decision; - // β†’ Approve | Reject | RequestRevision | Retry -} -``` - -**Skills**: -- Decision thresholds -- Evidence combination -- Feedback injection - -**Memory Integration**: -``` -DiagnoseIssueActivity { - issue: "judge consistently rejects step output", - returns: [ - "Tier 1: Check rubric alignment", - "Tier 2: Review judge logic history", - "Tier 3: See judge tuning guide" - ] -} -``` - ---- - -### Category 5: Model Provider Tools - -**Tool**: `ModelProvider Port` -```rust -pub trait ModelProvider { - fn run(&self, model_id: &str, prompt: &str, budget: &Budget) -> Result; -} -``` - -**Skills**: -- Model selection (when to use which model) -- Prompt engineering -- Token budgeting -- Error handling per model - -**Memory Integration - Prompt Optimization**: -``` -GetContextActivity { - tool: "model-provider", - task: "planner-step-generation", - returns: { - tier_1: "Known failure patterns for this step", - tier_2: "Successful prompt patterns", - tier_3: "Model capability guide", - } -} -// Use returned context to optimize prompt: -optimized_prompt = inject_learned_lessons( - base_prompt, - context.lessons, // "Always include edge cases for T1.3" - context.skills, // "Skill: planning-with-constraints" -) -``` - -**Skill Example: Prompt Template**: -```yaml -title: "Planner Step with Constraint Handling" -level: "L2" -content: | - You are a step planner for workflow execution. - - # Constraints (learned): - - Never generate steps without verification steps - - Include retry limits in plan - - Budget awareness required - - # Examples from memory (tier-2): - - Previous successful T1.3 outputs show pattern X - - Failed attempts shared pattern Y to avoid - - # Instructions: - Generate plan with these considerations... -``` - ---- - -### Category 6: Storage Tools - -**Tool**: `EventLog Port` (redb implementation) -```rust -pub trait EventLog { - fn append(&mut self, event: WorkerEvent) -> Result; - fn read(&self, range: Range) -> Result>; -} -``` - -**Skills**: -- Event serialization format -- Atomic writes -- Recovery from incomplete commits - -**Memory Integration**: -``` -LearnFromExecutionActivity { - taskID: "T0.5-eventlog-persistence", - result: "Redb backend successfully persisted 10K events", - tags: ["storage", "T0", "persistence"] -} -``` - -**Tool**: `BlobStore Port` (prompt/output capture) -```rust -pub trait BlobStore { - fn write(&self, path: &str, data: &[u8]) -> Result<()>; - fn read(&self, path: &str) -> Result>; -} -``` - -**Skills**: -- Path conventions (/{attempt_id}/{step_id}/prompt.txt) -- Compression strategies -- Retention policies - -**Memory Integration**: -``` -DocumentDecisionActivity { - decisionType: "blob-retention", - decision: "Archive attempts > 30 days to cold storage", - reasoning: "Balance audit trail with cost" -} -``` - ---- - -## Skills Ingestion Strategy - -### Phase 1: YAML Skills Registry - -**File**: `prompts/skills.yaml` -```yaml -skills: - - id: "kernel-state-machine" - category: "T0-kernel" - level: "L2" - title: "State Machine Kernel Patterns" - content: | - Key patterns for T0: - - Event log append-only design - - Atomic commit with 2PC - - Fold determinism for state derivation - - Fork/rewind for attempt recovery - - - id: "attempt-lifecycle" - category: "T1-execution" - level: "L2" - title: "Attempt Lifecycle Management" - content: | - Execution loop patterns: - - Poll-based task queue - - Budget tracking (tokens, attempts, time) - - Retry policy evaluation - - Context capture per attempt - - - id: "prompt-optimization" - category: "model-provider" - level: "L2" - title: "Memory-Based Prompt Optimization" - content: | - Best practices: - - Retrieve 3-tier context before execution - - Inject learned facts from tier-1 (exact matches) - - Include tier-2 patterns (ML-similar) - - Reference tier-3 docs (general guidance) - - Set budget constraints from experience -``` - -### Phase 2: Ingest Skills on Startup - -```go -// In cmd/starter/main.go - -func ingestSkills(memSvc *memory.Service) error { - skillsYAML, err := ioutil.ReadFile("prompts/skills.yaml") - if err != nil { - return err - } - - var skillsConfig struct { - Skills []struct { - ID string `yaml:"id"` - Category string `yaml:"category"` - Level string `yaml:"level"` - Title string `yaml:"title"` - Content string `yaml:"content"` - } `yaml:"skills"` - } - - if err := yaml.Unmarshal(skillsYAML, &skillsConfig); err != nil { - return err - } - - for _, skill := range skillsConfig.Skills { - _, err := memSvc.CreateKnowledge(ctx, &memory.KnowledgeRecord{ - Level: skill.Level, - Title: skill.Title, - Content: skill.Content, - Source: fmt.Sprintf("skills:///%s", skill.ID), - Metadata: map[string]interface{}{ - "skill_id": skill.ID, - "category": skill.Category, - "type": "skill", - }, - }) - if err != nil { - log.Warn(fmt.Sprintf("Failed to ingest skill %s: %v", skill.ID, err)) - continue - } - log.Info(fmt.Sprintf("Ingested skill: %s", skill.Title)) - } - - return nil -} -``` - -### Phase 3: Reference Docs Ingestion - -**File**: `poimen/crates/doc/` (Rust doc comments) - -```rust -/// # Attempt Lifecycle Pattern -/// -/// Every step execution follows this sequence: -/// 1. Check budget (tokens, attempts, time remaining) -/// 2. Retrieve context from memory (3-tier) -/// 3. Optimize prompt with lessons & skills -/// 4. Execute with ModelProvider -/// 5. Evaluate with Verifier -/// 6. Decide with Judge -/// 7. Learn (success) or Diagnose (failure) -/// 8. Retry or proceed to next step -/// -/// # Budget Tracking -/// - Tokens: Count LLM input/output tokens -/// - Attempts: Number of retries allowed -/// - Time: Wall-clock timeout per step -/// -/// # Retry Policy -/// - Exponential backoff: 1s β†’ 2s β†’ 4s -/// - Max attempts: 3 (configurable) -/// - Non-retryable: Syntax errors, auth failures -pub struct AttemptState { ... } -``` - -**Ingest Docs**: -```go -// Extract doc comments and ingest as L2 knowledge -// Run during build/startup: -// $ cargo doc --extract-comments | memory-ingest --level L2 -``` - -### Phase 4: Execution Pattern Capture - -```go -// In RunExecutor::execute_step() - -func (e *Executor) execute_step(ctx *WorkflowContext, step *StepId) error { - // ... execution logic ... - - // Capture pattern on success - if output.status == Success { - memSvc.CreateKnowledge(ctx, &memory.KnowledgeRecord{ - Level: "L1", - Title: fmt.Sprintf("Successful %s execution", step), - Content: fmt.Sprintf( - "Step %s completed with output:\n%s", - step, output.text, - ), - Source: fmt.Sprintf("workflow://execution/%s", step), - Metadata: map[string]interface{}{ - "step_id": step.String(), - "phase": ctx.PhaseId, - "attempt": ctx.AttemptState.Number, - "tokens_used": output.tokens, - }, - }) - } -} -``` - ---- - -## Tool-Skill Mapping Matrix - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Tool β†’ Skill Dependencies β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ Tool β”‚ Skills Needed (from memory) β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ WorkflowDef Builder β”‚ β€’ Phase dependencies β”‚ -β”‚ β”‚ β€’ IR canonicalization rules β”‚ -β”‚ β”‚ β€’ Schema versioning β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ Event Log β”‚ β€’ Event ordering guarantees β”‚ -β”‚ β”‚ β€’ Atomic commit protocol β”‚ -β”‚ β”‚ β€’ Checksum validation β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ Run Executor β”‚ β€’ Attempt lifecycle patterns β”‚ -β”‚ β”‚ β€’ Budget exhaustion detection β”‚ -β”‚ β”‚ β€’ Retry policy evaluation β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ Verifier Port β”‚ β€’ Rubric structure design β”‚ -β”‚ β”‚ β€’ Failure categorization β”‚ -β”‚ β”‚ β€’ Score aggregation rules β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ Judge Port β”‚ β€’ Decision thresholds β”‚ -β”‚ β”‚ β€’ Evidence combination logic β”‚ -β”‚ β”‚ β€’ Feedback injection patterns β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ ModelProvider β”‚ β€’ Prompt engineering best practices β”‚ -β”‚ β”‚ β€’ Token budget awareness β”‚ -β”‚ β”‚ β€’ Model-specific quirks β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ EventLog Storage β”‚ β€’ Serialization format choices β”‚ -β”‚ β”‚ β€’ Compression strategies β”‚ -β”‚ β”‚ β€’ Recovery procedures β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ BlobStore β”‚ β€’ Path naming conventions β”‚ -β”‚ β”‚ β€’ Retention policies β”‚ -β”‚ β”‚ β€’ Archive triggers β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - ---- - -## Basic Tool Usage Example - -### Scenario: Planner Step Fails Repeatedly - -**User Command**: -```bash -poimen plan my-workflow.yaml --phase T1 --retry-with-memory -``` - -**Tool Execution Chain**: - -``` -1. LOAD WORKFLOW - WorkflowDefBuilder.from_yaml("my-workflow.yaml") - β†’ Memory: Retrieve "IR-canonicalization" skills - β†’ Validate against stored L2 knowledge - -2. INIT EXECUTOR - RunExecutor.new() - β†’ Memory: Get "attempt-lifecycle" context - β†’ Load retry policy from memory lessons - -3. EXECUTE PLANNER STEP - for attempt in 1..max_attempts: - a) GetContextActivity - - Tool: "planner" - - Task: "step-generation" - - Returns: lessons + skills - - b) OptimizePrompt - - Inject learned facts (tier-1) - - Add pattern examples (tier-2) - - Set budget from history - - c) ModelProvider.run(optimized_prompt) - - Send to planner agent - - Wait for output - - d) Verifier.verify(output) - - Check against rubric - - Score output quality - - e) Judge.decide(output) - - Approve | Retry | Reject - - f) On Success: LearnFromExecutionActivity - - Store successful output pattern (L1) - - g) On Failure: AnalyzeErrorActivity - - Search for similar failures - - Return recovery suggestions - - h) DocumentDecisionActivity - - Log decision and reasoning - -4. COMPLETED - βœ… Plan generated (or user feedback required) - β†’ Memory: Ingest execution pattern - β†’ Next phase starts -``` - ---- - -## Summary: Tool & Skill Flow - -``` -Workflow Execution - ↓ - Tools Used ────────────────→ Skills Retrieved from Memory - β”œβ”€ WorkflowDefBuilder β”œβ”€ IR canonicalization rules - β”œβ”€ EventLog β”œβ”€ State machine patterns - β”œβ”€ RunExecutor β”œβ”€ Attempt lifecycle - β”œβ”€ Verifier Port β”œβ”€ Rubric design - β”œβ”€ Judge Port β”œβ”€ Decision logic - β”œβ”€ ModelProvider β”œβ”€ Prompt optimization - └─ Storage Ports └─ Retention policies - - Skills Guide Execution ──────→ Results Learned - β”œβ”€ Success patterns (L1) - β”œβ”€ Failure recovery (L1) - β”œβ”€ Verified practices (L2) - └─ Vault enriched for next run -``` - -This creates a **virtuous cycle**: Each execution improves the memory, which improves the next execution. diff --git a/action/analysis.go b/action/analysis.go new file mode 100644 index 0000000..3f823fe --- /dev/null +++ b/action/analysis.go @@ -0,0 +1,296 @@ +package action + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +// AnalyzeCodeInput is input for AnalyzeCodeActivity +type AnalyzeCodeInput struct { + Path string `json:"path"` + Language string `json:"language,omitempty"` + Depth int `json:"depth,omitempty"` +} + +// AnalyzeCodeOutput is output from AnalyzeCodeActivity +type AnalyzeCodeOutput struct { + Quality float64 `json:"quality"` + Metrics map[string]interface{} `json:"metrics"` + Issues []string `json:"issues"` + Summary string `json:"summary"` +} + +// AnalyzeCodeActivity analyzes code quality using available tools +func AnalyzeCodeActivity(ctx context.Context, in AnalyzeCodeInput) (AnalyzeCodeOutput, error) { + logger := newActivityLogger(ctx) + logger.Info("AnalyzeCodeActivity started", "path", in.Path) + + output := AnalyzeCodeOutput{ + Quality: 0.0, + Metrics: make(map[string]interface{}), + Issues: []string{}, + } + + // Verify path exists + if _, err := os.Stat(in.Path); os.IsNotExist(err) { + return output, fmt.Errorf("path does not exist: %s", in.Path) + } + + // Count files and lines + var totalFiles, totalLines int + err := filepath.Walk(in.Path, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil // Skip errors + } + if info.IsDir() { + // Skip hidden and vendor directories + if strings.HasPrefix(info.Name(), ".") || info.Name() == "vendor" || info.Name() == "node_modules" { + return filepath.SkipDir + } + return nil + } + + ext := filepath.Ext(path) + if isCodeFile(ext) { + totalFiles++ + if lines, err := countLines(path); err == nil { + totalLines += lines + } + } + return nil + }) + if err != nil { + logger.Warn("Error walking path", "error", err) + } + + output.Metrics["totalFiles"] = totalFiles + output.Metrics["totalLines"] = totalLines + + // Try to run go vet if it's a Go project + if _, err := os.Stat(filepath.Join(in.Path, "go.mod")); err == nil { + cmd := exec.CommandContext(ctx, "go", "vet", "./...") + cmd.Dir = in.Path + vetOutput, err := cmd.CombinedOutput() + if err != nil { + issues := strings.Split(string(vetOutput), "\n") + for _, issue := range issues { + if strings.TrimSpace(issue) != "" { + output.Issues = append(output.Issues, issue) + } + } + } + output.Metrics["goVetRan"] = true + } + + // Calculate quality score (simple heuristic) + issueCount := len(output.Issues) + if totalFiles > 0 { + issuesPerFile := float64(issueCount) / float64(totalFiles) + output.Quality = max(0, 1.0 - (issuesPerFile * 0.1)) + } else { + output.Quality = 0.5 + } + + output.Summary = fmt.Sprintf("Analyzed %d files (%d lines). Found %d issues. Quality score: %.2f", + totalFiles, totalLines, issueCount, output.Quality) + + logger.Info("AnalyzeCodeActivity completed", "quality", output.Quality, "issues", issueCount) + return output, nil +} + +// SecurityScanInput is input for SecurityScanActivity +type SecurityScanInput struct { + Path string `json:"path"` + Severity string `json:"severity,omitempty"` // low, medium, high, critical +} + +// SecurityScanOutput is output from SecurityScanActivity +type SecurityScanOutput struct { + Vulnerabilities []Vulnerability `json:"vulnerabilities"` + SecurityScore float64 `json:"securityScore"` + RiskLevel string `json:"riskLevel"` +} + +// Vulnerability represents a security issue +type Vulnerability struct { + ID string `json:"id"` + Severity string `json:"severity"` + Description string `json:"description"` + File string `json:"file,omitempty"` + Line int `json:"line,omitempty"` +} + +// SecurityScanActivity scans code for security vulnerabilities +func SecurityScanActivity(ctx context.Context, in SecurityScanInput) (SecurityScanOutput, error) { + logger := newActivityLogger(ctx) + logger.Info("SecurityScanActivity started", "path", in.Path) + + output := SecurityScanOutput{ + Vulnerabilities: []Vulnerability{}, + SecurityScore: 100.0, + RiskLevel: "low", + } + + // Verify path exists + if _, err := os.Stat(in.Path); os.IsNotExist(err) { + return output, fmt.Errorf("path does not exist: %s", in.Path) + } + + // Check for common security issues + // 1. Check for hardcoded secrets + secretPatterns := []string{ + "password=", + "secret=", + "api_key=", + "apikey=", + "private_key", + "AWS_SECRET", + } + + err := filepath.Walk(in.Path, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return nil + } + + if !isCodeFile(filepath.Ext(path)) { + return nil + } + + content, err := os.ReadFile(path) + if err != nil { + return nil + } + + contentStr := strings.ToLower(string(content)) + for _, pattern := range secretPatterns { + if strings.Contains(contentStr, pattern) { + output.Vulnerabilities = append(output.Vulnerabilities, Vulnerability{ + ID: fmt.Sprintf("SEC-%d", len(output.Vulnerabilities)+1), + Severity: "high", + Description: fmt.Sprintf("Possible hardcoded secret: %s", pattern), + File: path, + }) + } + } + return nil + }) + if err != nil { + logger.Warn("Error scanning", "error", err) + } + + // Try gosec if available and it's a Go project + if _, err := os.Stat(filepath.Join(in.Path, "go.mod")); err == nil { + if _, err := exec.LookPath("gosec"); err == nil { + cmd := exec.CommandContext(ctx, "gosec", "-fmt=json", "-quiet", "./...") + cmd.Dir = in.Path + // gosec returns non-zero if issues found, so we ignore the error + cmd.CombinedOutput() + } + } + + // Calculate score + vulnCount := len(output.Vulnerabilities) + if vulnCount == 0 { + output.SecurityScore = 100.0 + output.RiskLevel = "low" + } else if vulnCount < 3 { + output.SecurityScore = 80.0 + output.RiskLevel = "medium" + } else if vulnCount < 10 { + output.SecurityScore = 50.0 + output.RiskLevel = "high" + } else { + output.SecurityScore = 20.0 + output.RiskLevel = "critical" + } + + logger.Info("SecurityScanActivity completed", "vulnerabilities", vulnCount, "riskLevel", output.RiskLevel) + return output, nil +} + +// GenerateReportInput is input for GenerateReportActivity +type GenerateReportInput struct { + AnalysisResult interface{} `json:"analysisResult"` + SecurityResult interface{} `json:"securityResult"` + Format string `json:"format,omitempty"` // markdown, html, json +} + +// GenerateReportOutput is output from GenerateReportActivity +type GenerateReportOutput struct { + Report string `json:"report"` + ReportPath string `json:"reportPath"` +} + +// GenerateReportActivity generates a combined report +func GenerateReportActivity(ctx context.Context, in GenerateReportInput) (GenerateReportOutput, error) { + logger := newActivityLogger(ctx) + logger.Info("GenerateReportActivity started") + + format := in.Format + if format == "" { + format = "markdown" + } + + var report strings.Builder + timestamp := time.Now().Format(time.RFC3339) + + switch format { + case "markdown": + report.WriteString("# Analysis Report\n\n") + report.WriteString(fmt.Sprintf("Generated: %s\n\n", timestamp)) + report.WriteString("## Code Analysis\n\n") + report.WriteString(fmt.Sprintf("```\n%v\n```\n\n", in.AnalysisResult)) + report.WriteString("## Security Scan\n\n") + report.WriteString(fmt.Sprintf("```\n%v\n```\n\n", in.SecurityResult)) + case "json": + report.WriteString(fmt.Sprintf(`{"timestamp":"%s","analysis":%v,"security":%v}`, + timestamp, in.AnalysisResult, in.SecurityResult)) + default: + report.WriteString(fmt.Sprintf("Report generated at %s\n", timestamp)) + report.WriteString(fmt.Sprintf("Analysis: %v\n", in.AnalysisResult)) + report.WriteString(fmt.Sprintf("Security: %v\n", in.SecurityResult)) + } + + // Save to temp file + reportPath := filepath.Join(os.TempDir(), fmt.Sprintf("report-%d.%s", time.Now().UnixNano(), format)) + if err := os.WriteFile(reportPath, []byte(report.String()), 0644); err != nil { + logger.Warn("Failed to save report", "error", err) + } + + logger.Info("GenerateReportActivity completed", "format", format) + return GenerateReportOutput{ + Report: report.String(), + ReportPath: reportPath, + }, nil +} + +// Helper functions + +func isCodeFile(ext string) bool { + codeExts := map[string]bool{ + ".go": true, ".py": true, ".js": true, ".ts": true, + ".java": true, ".c": true, ".cpp": true, ".h": true, + ".rs": true, ".rb": true, ".php": true, ".swift": true, + } + return codeExts[ext] +} + +func countLines(path string) (int, error) { + content, err := os.ReadFile(path) + if err != nil { + return 0, err + } + return len(strings.Split(string(content), "\n")), nil +} + +func max(a, b float64) float64 { + if a > b { + return a + } + return b +} diff --git a/action/analysis_test.go b/action/analysis_test.go new file mode 100644 index 0000000..0914d32 --- /dev/null +++ b/action/analysis_test.go @@ -0,0 +1,144 @@ +package action + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAnalyzeCodeActivity(t *testing.T) { + // Create temp directory with some Go code + tmpDir, err := os.MkdirTemp("", "analyze-test") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + // Create go.mod + goMod := `module test +go 1.21 +` + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte(goMod), 0644)) + + // Create a simple Go file + goCode := `package main + +func main() { + println("hello") +} +` + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte(goCode), 0644)) + + // Run activity + output, err := AnalyzeCodeActivity(context.Background(), AnalyzeCodeInput{ + Path: tmpDir, + Depth: 3, + }) + require.NoError(t, err) + + // Verify output + require.Greater(t, output.Quality, 0.0) + require.NotNil(t, output.Metrics) + require.NotEmpty(t, output.Summary) + + totalFiles, ok := output.Metrics["totalFiles"].(int) + require.True(t, ok) + require.Equal(t, 1, totalFiles) // Just main.go (go.mod not counted) +} + +func TestAnalyzeCodeActivity_PathNotExist(t *testing.T) { + _, err := AnalyzeCodeActivity(context.Background(), AnalyzeCodeInput{ + Path: "/nonexistent/path", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "does not exist") +} + +func TestSecurityScanActivity(t *testing.T) { + // Create temp directory + tmpDir, err := os.MkdirTemp("", "security-test") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + // Create a file with potential secret (matches pattern "password=") + code := `package main + +var config = map[string]string{ + "password=": "supersecret123", + "api_key=": "sk-12345", +} +` + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte(code), 0644)) + + // Run activity + output, err := SecurityScanActivity(context.Background(), SecurityScanInput{ + Path: tmpDir, + Severity: "medium", + }) + require.NoError(t, err) + + // Should find the hardcoded password + require.Greater(t, len(output.Vulnerabilities), 0) + require.Less(t, output.SecurityScore, 100.0) +} + +func TestSecurityScanActivity_Clean(t *testing.T) { + // Create temp directory with clean code + tmpDir, err := os.MkdirTemp("", "security-clean-test") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + // Create clean code + code := `package main + +func main() { + println("hello") +} +` + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte(code), 0644)) + + // Run activity + output, err := SecurityScanActivity(context.Background(), SecurityScanInput{ + Path: tmpDir, + }) + require.NoError(t, err) + + // Should be clean + require.Equal(t, 0, len(output.Vulnerabilities)) + require.Equal(t, 100.0, output.SecurityScore) + require.Equal(t, "low", output.RiskLevel) +} + +func TestGenerateReportActivity(t *testing.T) { + output, err := GenerateReportActivity(context.Background(), GenerateReportInput{ + AnalysisResult: map[string]interface{}{"quality": 0.85}, + SecurityResult: map[string]interface{}{"score": 95.0}, + Format: "markdown", + }) + require.NoError(t, err) + + require.Contains(t, output.Report, "# Analysis Report") + require.NotEmpty(t, output.ReportPath) + + // Verify file was created + _, err = os.Stat(output.ReportPath) + require.NoError(t, err) + + // Cleanup + os.Remove(output.ReportPath) +} + +func TestGenerateReportActivity_JSON(t *testing.T) { + output, err := GenerateReportActivity(context.Background(), GenerateReportInput{ + AnalysisResult: map[string]interface{}{"quality": 0.85}, + SecurityResult: map[string]interface{}{"score": 95.0}, + Format: "json", + }) + require.NoError(t, err) + + require.Contains(t, output.Report, `"timestamp"`) + + // Cleanup + os.Remove(output.ReportPath) +} diff --git a/action/logger.go b/action/logger.go new file mode 100644 index 0000000..9b806f2 --- /dev/null +++ b/action/logger.go @@ -0,0 +1,49 @@ +package action + +import ( + "context" + "log" + + "go.temporal.io/sdk/activity" +) + +// activityLogger provides logging that works both in and outside Temporal context +type activityLogger struct { + ctx context.Context +} + +func newActivityLogger(ctx context.Context) *activityLogger { + return &activityLogger{ctx: ctx} +} + +func (l *activityLogger) Info(msg string, args ...interface{}) { + if activity.IsActivity(l.ctx) { + activity.GetLogger(l.ctx).Info(msg, args...) + } else { + log.Printf("INFO: "+msg+" %v", args) + } +} + +func (l *activityLogger) Warn(msg string, args ...interface{}) { + if activity.IsActivity(l.ctx) { + activity.GetLogger(l.ctx).Warn(msg, args...) + } else { + log.Printf("WARN: "+msg+" %v", args) + } +} + +func (l *activityLogger) Error(msg string, args ...interface{}) { + if activity.IsActivity(l.ctx) { + activity.GetLogger(l.ctx).Error(msg, args...) + } else { + log.Printf("ERROR: "+msg+" %v", args) + } +} + +func (l *activityLogger) Debug(msg string, args ...interface{}) { + if activity.IsActivity(l.ctx) { + activity.GetLogger(l.ctx).Debug(msg, args...) + } else { + log.Printf("DEBUG: "+msg+" %v", args) + } +} diff --git a/action/memory.go b/action/memory.go new file mode 100644 index 0000000..fafe76f --- /dev/null +++ b/action/memory.go @@ -0,0 +1,255 @@ +package action + +import ( + "context" + "fmt" + "os" + + "github.com/rockliang/poimen/workflows/internal/memory" +) + +// RetrieveMemoryInput input for RetrieveMemoryActivity +type RetrieveMemoryInput struct { + // Query semantic search query + Query string `json:"query"` + + // Project memory project (default: "poimen") + Project string `json:"project,omitempty"` + + // Scope retrieval scope: "skills", "lessons", "all" (default: "all") + Scope string `json:"scope,omitempty"` + + // Limit max results (default: 10) + Limit int `json:"limit,omitempty"` + + // LevelFilter filter by level: L1, L2, R (reference) + LevelFilter []string `json:"levelFilter,omitempty"` + + // Tool tool context for skill matching + Tool string `json:"tool,omitempty"` + + // Task task description for context retrieval + Task string `json:"task,omitempty"` +} + +// RetrieveMemoryOutput output from RetrieveMemoryActivity +type RetrieveMemoryOutput struct { + // Skills relevant skills found + Skills []MemorySkill `json:"skills"` + + // Lessons relevant lessons/knowledge found + Lessons []MemoryLesson `json:"lessons"` + + // References reference documents found + References []MemoryReference `json:"references"` + + // TotalResults total results found + TotalResults int `json:"totalResults"` + + // Budget token budget info + Budget MemoryBudget `json:"budget"` +} + +// MemorySkill skill from memory +type MemorySkill struct { + Name string `json:"name"` + Description string `json:"description"` + Why string `json:"why,omitempty"` +} + +// MemoryLesson lesson from memory +type MemoryLesson struct { + ID string `json:"id"` + Text string `json:"text"` + Level string `json:"level"` + Score float32 `json:"score"` + Breadcrumb string `json:"breadcrumb,omitempty"` +} + +// MemoryReference reference document from memory +type MemoryReference struct { + ID string `json:"id"` + Text string `json:"text"` + Score float32 `json:"score"` + Breadcrumb string `json:"breadcrumb,omitempty"` +} + +// MemoryBudget token budget tracking +type MemoryBudget struct { + Requested int `json:"requested"` + Used int `json:"used"` +} + +// RetrieveMemoryActivity retrieves relevant knowledge from poimen-memory +func RetrieveMemoryActivity(ctx context.Context, in RetrieveMemoryInput) (RetrieveMemoryOutput, error) { + logger := newActivityLogger(ctx) + logger.Info("RetrieveMemoryActivity started", "query", in.Query, "scope", in.Scope) + + output := RetrieveMemoryOutput{ + Skills: []MemorySkill{}, + Lessons: []MemoryLesson{}, + References: []MemoryReference{}, + } + + // Get memory service URL and token + baseURL := os.Getenv("POIMEN_MEMORY_URL") + if baseURL == "" { + baseURL = "http://poimen-memory.poimen.svc.cluster.local:8080" + } + + token := os.Getenv("POIMEN_MEMORY_TOKEN") + // Token optional for internal cluster access + + // Set defaults + project := in.Project + if project == "" { + project = "poimen" + } + + scope := in.Scope + if scope == "" { + scope = "all" + } + + limit := in.Limit + if limit == 0 { + limit = 10 + } + + client := memory.NewClient(baseURL, token) + + // If tool/task provided, use Context API for skill matching + if in.Tool != "" || in.Task != "" { + contextResp, err := client.Context(ctx, &memory.ContextRequest{ + Project: project, + Tool: in.Tool, + Task: in.Task, + SignatureSource: in.Query, + Scope: "tool_context", + Budget: 8192, + }) + if err != nil { + logger.Warn("Context retrieval failed, falling back to query", "error", err) + } else { + // Extract skills + for _, skill := range contextResp.Skills { + output.Skills = append(output.Skills, MemorySkill{ + Name: skill.Name, + Why: skill.Why, + }) + } + + // Extract lessons + for i, lesson := range contextResp.Lessons { + output.Lessons = append(output.Lessons, MemoryLesson{ + ID: fmt.Sprintf("ctx-%d", i), + Text: lesson.Text, + Level: lesson.Level, + Score: lesson.Score, + Breadcrumb: "", + }) + } + + output.Budget = MemoryBudget{ + Requested: contextResp.Budget.Requested, + Used: contextResp.Budget.Used, + } + output.TotalResults = len(output.Skills) + len(output.Lessons) + } + } + + // Also do semantic query for additional context + if scope == "all" || scope == "lessons" || scope == "references" { + levelFilter := in.LevelFilter + if len(levelFilter) == 0 { + levelFilter = []string{"L1", "L2"} + } + + queryResp, err := client.Query(ctx, &memory.QueryRequest{ + Project: project, + Query: in.Query, + LevelFilter: levelFilter, + Limit: limit, + Scope: "all", + }) + if err != nil { + logger.Warn("Query failed", "error", err) + } else { + for _, result := range queryResp.Results { + if result.Level == "R" { + output.References = append(output.References, MemoryReference{ + ID: result.ID, + Text: result.Text, + Score: result.Score, + Breadcrumb: result.Breadcrumb, + }) + } else { + // Avoid duplicates from Context call + found := false + for _, existing := range output.Lessons { + if existing.ID == result.ID { + found = true + break + } + } + if !found { + output.Lessons = append(output.Lessons, MemoryLesson{ + ID: result.ID, + Text: result.Text, + Level: result.Level, + Score: result.Score, + Breadcrumb: result.Breadcrumb, + }) + } + } + } + output.TotalResults = len(output.Skills) + len(output.Lessons) + len(output.References) + } + } + + logger.Info("RetrieveMemoryActivity completed", + "skills", len(output.Skills), + "lessons", len(output.Lessons), + "references", len(output.References)) + + return output, nil +} + +// FormatMemoryForPrompt formats memory output for LLM prompt injection +func FormatMemoryForPrompt(mem RetrieveMemoryOutput) string { + if mem.TotalResults == 0 { + return "" + } + + var result string + + if len(mem.Skills) > 0 { + result += "\n## Relevant Skills\n" + for _, skill := range mem.Skills { + result += fmt.Sprintf("- **%s**: %s\n", skill.Name, skill.Why) + } + } + + if len(mem.Lessons) > 0 { + result += "\n## Relevant Knowledge\n" + for _, lesson := range mem.Lessons { + result += fmt.Sprintf("- [%s] %s\n", lesson.Level, truncate(lesson.Text, 200)) + } + } + + if len(mem.References) > 0 { + result += "\n## Reference Documents\n" + for _, ref := range mem.References { + result += fmt.Sprintf("- %s\n", truncate(ref.Text, 200)) + } + } + + return result +} + +func truncate(s string, maxLen int) string { + if len(s) <= maxLen { + return s + } + return s[:maxLen] + "..." +} diff --git a/action/memory_test.go b/action/memory_test.go new file mode 100644 index 0000000..072b06e --- /dev/null +++ b/action/memory_test.go @@ -0,0 +1,138 @@ +package action + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRetrieveMemoryActivity_Query(t *testing.T) { + // Mock memory service + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/memory/query" { + resp := map[string]interface{}{ + "results": []map[string]interface{}{ + { + "id": "doc-1", + "level": "L1", + "score": 0.95, + "text": "Security scanning best practices: always check for hardcoded secrets", + }, + { + "id": "doc-2", + "level": "L2", + "score": 0.85, + "text": "Use gosec for Go security analysis", + }, + }, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + return + } + http.NotFound(w, r) + })) + defer server.Close() + + // Set env for test + t.Setenv("POIMEN_MEMORY_URL", server.URL) + + output, err := RetrieveMemoryActivity(context.Background(), RetrieveMemoryInput{ + Query: "security scanning", + Project: "poimen", + Scope: "lessons", + Limit: 5, + }) + require.NoError(t, err) + + require.Equal(t, 2, len(output.Lessons)) + require.Equal(t, "L1", output.Lessons[0].Level) + require.Contains(t, output.Lessons[0].Text, "Security scanning") +} + +func TestRetrieveMemoryActivity_Context(t *testing.T) { + // Mock memory service with context endpoint + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/memory/context" { + resp := map[string]interface{}{ + "tier": 1, + "skills": []map[string]interface{}{ + { + "name": "security-analysis", + "why": "User is asking about security scanning", + }, + }, + "lessons": []map[string]interface{}{ + { + "tier": 1, + "level": "L1", + "score": 0.9, + "text": "Always scan dependencies for vulnerabilities", + }, + }, + "budget": map[string]interface{}{ + "requested": 8192, + "used": 1024, + }, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + return + } + if r.URL.Path == "/memory/query" { + resp := map[string]interface{}{"results": []interface{}{}} + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + return + } + http.NotFound(w, r) + })) + defer server.Close() + + t.Setenv("POIMEN_MEMORY_URL", server.URL) + + output, err := RetrieveMemoryActivity(context.Background(), RetrieveMemoryInput{ + Query: "security scan repo", + Tool: "poimen-router", + Task: "generate workflow for security scanning", + }) + require.NoError(t, err) + + require.Equal(t, 1, len(output.Skills)) + require.Equal(t, "security-analysis", output.Skills[0].Name) + require.Equal(t, 1, len(output.Lessons)) + require.Equal(t, 8192, output.Budget.Requested) + require.Equal(t, 1024, output.Budget.Used) +} + +func TestFormatMemoryForPrompt(t *testing.T) { + mem := RetrieveMemoryOutput{ + Skills: []MemorySkill{ + {Name: "security-scan", Description: "Run security scanner", Why: "Matches user intent"}, + }, + Lessons: []MemoryLesson{ + {ID: "1", Level: "L1", Text: "Always check dependencies"}, + }, + TotalResults: 2, + } + + result := FormatMemoryForPrompt(mem) + + require.Contains(t, result, "## Relevant Skills") + require.Contains(t, result, "security-scan") + require.Contains(t, result, "## Relevant Knowledge") + require.Contains(t, result, "Always check dependencies") +} + +func TestFormatMemoryForPrompt_Empty(t *testing.T) { + mem := RetrieveMemoryOutput{ + TotalResults: 0, + } + + result := FormatMemoryForPrompt(mem) + require.Equal(t, "", result) +} diff --git a/action/notification.go b/action/notification.go new file mode 100644 index 0000000..947ffcc --- /dev/null +++ b/action/notification.go @@ -0,0 +1,298 @@ +package action + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "os" + "os/exec" + "time" +) + +// NotifyStatusInput is input for NotifyStatusActivity +type NotifyStatusInput struct { + Channel string `json:"channel"` // slack, email, webhook + Status string `json:"status"` // success, failure, warning + Message string `json:"message"` +} + +// NotifyStatusOutput is output from NotifyStatusActivity +type NotifyStatusOutput struct { + NotificationID string `json:"notificationId"` + Timestamp string `json:"timestamp"` +} + +// NotifyStatusActivity sends notifications +func NotifyStatusActivity(ctx context.Context, in NotifyStatusInput) (NotifyStatusOutput, error) { + logger := newActivityLogger(ctx) + logger.Info("NotifyStatusActivity started", "channel", in.Channel, "status", in.Status) + + timestamp := time.Now().Format(time.RFC3339) + notificationID := fmt.Sprintf("notify-%d", time.Now().UnixNano()) + + switch in.Channel { + case "slack": + if err := sendSlackNotification(ctx, in); err != nil { + logger.Warn("Slack notification failed", "error", err) + // Don't fail the activity, just log + } + case "webhook": + if err := sendWebhookNotification(ctx, in); err != nil { + logger.Warn("Webhook notification failed", "error", err) + } + case "email": + // Email would require SMTP setup - log for now + logger.Info("Email notification (logged)", "message", in.Message) + default: + logger.Info("Notification logged", "channel", in.Channel, "message", in.Message) + } + + logger.Info("NotifyStatusActivity completed", "notificationId", notificationID) + return NotifyStatusOutput{ + NotificationID: notificationID, + Timestamp: timestamp, + }, nil +} + +func sendSlackNotification(ctx context.Context, in NotifyStatusInput) error { + webhookURL := os.Getenv("SLACK_WEBHOOK_URL") + if webhookURL == "" { + return fmt.Errorf("SLACK_WEBHOOK_URL not set") + } + + // Map status to emoji + emoji := "ℹ️" + switch in.Status { + case "success": + emoji = "βœ…" + case "failure": + emoji = "❌" + case "warning": + emoji = "⚠️" + } + + payload := map[string]string{ + "text": fmt.Sprintf("%s *%s*: %s", emoji, in.Status, in.Message), + } + + body, _ := json.Marshal(payload) + req, err := http.NewRequestWithContext(ctx, "POST", webhookURL, bytes.NewBuffer(body)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("slack returned status %d", resp.StatusCode) + } + + return nil +} + +func sendWebhookNotification(ctx context.Context, in NotifyStatusInput) error { + webhookURL := os.Getenv("NOTIFICATION_WEBHOOK_URL") + if webhookURL == "" { + return fmt.Errorf("NOTIFICATION_WEBHOOK_URL not set") + } + + payload := map[string]interface{}{ + "channel": in.Channel, + "status": in.Status, + "message": in.Message, + "timestamp": time.Now().Format(time.RFC3339), + } + + body, _ := json.Marshal(payload) + req, err := http.NewRequestWithContext(ctx, "POST", webhookURL, bytes.NewBuffer(body)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + return fmt.Errorf("webhook returned status %d", resp.StatusCode) + } + + return nil +} + +// ArchiveResultsInput is input for ArchiveResultsActivity +type ArchiveResultsInput struct { + ReportPath string `json:"reportPath"` + Destination string `json:"destination"` // s3://bucket/path or local path + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +// ArchiveResultsOutput is output from ArchiveResultsActivity +type ArchiveResultsOutput struct { + ArchiveURL string `json:"archiveUrl"` + ArchiveSize int64 `json:"archiveSize"` +} + +// ArchiveResultsActivity archives results to storage +func ArchiveResultsActivity(ctx context.Context, in ArchiveResultsInput) (ArchiveResultsOutput, error) { + logger := newActivityLogger(ctx) + logger.Info("ArchiveResultsActivity started", "reportPath", in.ReportPath, "destination", in.Destination) + + // Check if source exists + info, err := os.Stat(in.ReportPath) + if err != nil { + return ArchiveResultsOutput{}, fmt.Errorf("report not found: %s", in.ReportPath) + } + + // For now, just copy to local destination or log for cloud + var archiveURL string + var archiveSize int64 + + if len(in.Destination) > 5 && in.Destination[:5] == "s3://" { + // Would use AWS SDK - for now just log + logger.Info("Would upload to S3", "destination", in.Destination) + archiveURL = in.Destination + archiveSize = info.Size() + } else { + // Copy to local destination + content, err := os.ReadFile(in.ReportPath) + if err != nil { + return ArchiveResultsOutput{}, fmt.Errorf("failed to read report: %w", err) + } + + destPath := in.Destination + if destPath == "" { + destPath = fmt.Sprintf("/tmp/archive-%d", time.Now().UnixNano()) + } + + if err := os.WriteFile(destPath, content, 0644); err != nil { + return ArchiveResultsOutput{}, fmt.Errorf("failed to write archive: %w", err) + } + + archiveURL = destPath + archiveSize = int64(len(content)) + } + + logger.Info("ArchiveResultsActivity completed", "archiveUrl", archiveURL, "size", archiveSize) + return ArchiveResultsOutput{ + ArchiveURL: archiveURL, + ArchiveSize: archiveSize, + }, nil +} + +// DeploymentPreCheckInput is input for DeploymentPreCheckActivity +type DeploymentPreCheckInput struct { + Path string `json:"path"` + CheckType string `json:"checkType,omitempty"` // lint, test, build, all +} + +// DeploymentPreCheckOutput is output from DeploymentPreCheckActivity +type DeploymentPreCheckOutput struct { + Passed bool `json:"passed"` + Failures []string `json:"failures"` + Warnings []string `json:"warnings"` +} + +// DeploymentPreCheckActivity validates deployment readiness +func DeploymentPreCheckActivity(ctx context.Context, in DeploymentPreCheckInput) (DeploymentPreCheckOutput, error) { + logger := newActivityLogger(ctx) + logger.Info("DeploymentPreCheckActivity started", "path", in.Path, "checkType", in.CheckType) + + output := DeploymentPreCheckOutput{ + Passed: true, + Failures: []string{}, + Warnings: []string{}, + } + + checkType := in.CheckType + if checkType == "" { + checkType = "all" + } + + // Check if path exists + if _, err := os.Stat(in.Path); os.IsNotExist(err) { + output.Passed = false + output.Failures = append(output.Failures, fmt.Sprintf("path does not exist: %s", in.Path)) + return output, nil + } + + // Check for Go project + isGo := false + if _, err := os.Stat(fmt.Sprintf("%s/go.mod", in.Path)); err == nil { + isGo = true + } + + if isGo && (checkType == "all" || checkType == "build") { + // Try go build + cmd := exec.CommandContext(ctx, "go", "build", "./...") + cmd.Dir = in.Path + if buildOut, err := cmd.CombinedOutput(); err != nil { + output.Passed = false + output.Failures = append(output.Failures, fmt.Sprintf("build failed: %s", string(buildOut))) + } + } + + if isGo && (checkType == "all" || checkType == "test") { + // Try go test + cmd := exec.CommandContext(ctx, "go", "test", "-short", "./...") + cmd.Dir = in.Path + if testOut, err := cmd.CombinedOutput(); err != nil { + output.Passed = false + output.Failures = append(output.Failures, fmt.Sprintf("tests failed: %s", string(testOut))) + } + } + + if isGo && (checkType == "all" || checkType == "lint") { + // Try go vet + cmd := exec.CommandContext(ctx, "go", "vet", "./...") + cmd.Dir = in.Path + if vetOut, err := cmd.CombinedOutput(); err != nil { + output.Warnings = append(output.Warnings, fmt.Sprintf("vet issues: %s", string(vetOut))) + } + } + + logger.Info("DeploymentPreCheckActivity completed", "passed", output.Passed, "failures", len(output.Failures)) + return output, nil +} + +// ApproveWorkflowInput is input for ApproveWorkflowActivity +type ApproveWorkflowInput struct { + WorkflowID string `json:"workflowId"` + RequiredApprovals int `json:"requiredApprovals,omitempty"` + TimeoutMinutes int `json:"timeoutMinutes,omitempty"` +} + +// ApproveWorkflowOutput is output from ApproveWorkflowActivity +type ApproveWorkflowOutput struct { + Approved bool `json:"approved"` + Approver string `json:"approver,omitempty"` + Timestamp string `json:"timestamp"` +} + +// ApproveWorkflowActivity handles approval workflow (auto-approves for now) +func ApproveWorkflowActivity(ctx context.Context, in ApproveWorkflowInput) (ApproveWorkflowOutput, error) { + logger := newActivityLogger(ctx) + logger.Info("ApproveWorkflowActivity started", "workflowId", in.WorkflowID) + + // For now, auto-approve + // In production, this would wait for human approval via signal or external system + + timestamp := time.Now().Format(time.RFC3339) + + logger.Info("ApproveWorkflowActivity completed (auto-approved)") + return ApproveWorkflowOutput{ + Approved: true, + Approver: "system-auto", + Timestamp: timestamp, + }, nil +} diff --git a/action/notification_test.go b/action/notification_test.go new file mode 100644 index 0000000..22ace49 --- /dev/null +++ b/action/notification_test.go @@ -0,0 +1,130 @@ +package action + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNotifyStatusActivity(t *testing.T) { + // Test with default channel (logs only) + output, err := NotifyStatusActivity(context.Background(), NotifyStatusInput{ + Channel: "log", + Status: "success", + Message: "Test notification", + }) + require.NoError(t, err) + + require.NotEmpty(t, output.NotificationID) + require.NotEmpty(t, output.Timestamp) +} + +func TestNotifyStatusActivity_AllStatuses(t *testing.T) { + statuses := []string{"success", "failure", "warning"} + + for _, status := range statuses { + t.Run(status, func(t *testing.T) { + output, err := NotifyStatusActivity(context.Background(), NotifyStatusInput{ + Channel: "log", + Status: status, + Message: "Test " + status, + }) + require.NoError(t, err) + require.NotEmpty(t, output.NotificationID) + }) + } +} + +func TestArchiveResultsActivity(t *testing.T) { + // Create temp source file + tmpDir, err := os.MkdirTemp("", "archive-test") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + srcPath := filepath.Join(tmpDir, "report.txt") + require.NoError(t, os.WriteFile(srcPath, []byte("test report content"), 0644)) + + // Archive to local destination + destPath := filepath.Join(tmpDir, "archive.txt") + output, err := ArchiveResultsActivity(context.Background(), ArchiveResultsInput{ + ReportPath: srcPath, + Destination: destPath, + }) + require.NoError(t, err) + + require.Equal(t, destPath, output.ArchiveURL) + require.Greater(t, output.ArchiveSize, int64(0)) + + // Verify file was copied + content, err := os.ReadFile(destPath) + require.NoError(t, err) + require.Equal(t, "test report content", string(content)) +} + +func TestArchiveResultsActivity_NotFound(t *testing.T) { + _, err := ArchiveResultsActivity(context.Background(), ArchiveResultsInput{ + ReportPath: "/nonexistent/file.txt", + Destination: "/tmp/archive.txt", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "not found") +} + +func TestDeploymentPreCheckActivity(t *testing.T) { + // Create temp directory with valid Go code + tmpDir, err := os.MkdirTemp("", "precheck-test") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + // Create go.mod + goMod := `module test +go 1.21 +` + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte(goMod), 0644)) + + // Create valid Go file + goCode := `package main + +func main() { + println("hello") +} +` + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte(goCode), 0644)) + + // Run pre-check + output, err := DeploymentPreCheckActivity(context.Background(), DeploymentPreCheckInput{ + Path: tmpDir, + CheckType: "build", + }) + require.NoError(t, err) + + require.True(t, output.Passed) + require.Empty(t, output.Failures) +} + +func TestDeploymentPreCheckActivity_PathNotExist(t *testing.T) { + output, err := DeploymentPreCheckActivity(context.Background(), DeploymentPreCheckInput{ + Path: "/nonexistent/path", + }) + require.NoError(t, err) // Activity doesn't error, just reports failure + + require.False(t, output.Passed) + require.NotEmpty(t, output.Failures) +} + +func TestApproveWorkflowActivity(t *testing.T) { + output, err := ApproveWorkflowActivity(context.Background(), ApproveWorkflowInput{ + WorkflowID: "test-workflow-123", + RequiredApprovals: 1, + TimeoutMinutes: 60, + }) + require.NoError(t, err) + + // Auto-approved + require.True(t, output.Approved) + require.Equal(t, "system-auto", output.Approver) + require.NotEmpty(t, output.Timestamp) +} diff --git a/action/router.go b/action/router.go new file mode 100644 index 0000000..ed9bcc6 --- /dev/null +++ b/action/router.go @@ -0,0 +1,89 @@ +package action + +import ( + "context" + "fmt" + + "github.com/rockliang/poimen/workflows/internal/routing" + "go.temporal.io/sdk/activity" +) + +// LLMRouterActivity is the Temporal activity that routes user requests to workflows +func LLMRouterActivity(ctx context.Context, input routing.LLMRouterInput) (*routing.LLMRouterOutput, error) { + logger := activity.GetLogger(ctx) + logger.Info("LLMRouterActivity started", "message", input.Message) + + // Load knowledge base + kb, err := routing.LoadKnowledgeBaseFromDefaultPath() + if err != nil { + return nil, fmt.Errorf("failed to load knowledge base: %w", err) + } + + // Create router + router, err := routing.NewLLMRouter(kb) + if err != nil { + return nil, fmt.Errorf("failed to create router: %w", err) + } + + // Route the request + output, err := router.Route(ctx, input) + if err != nil { + logger.Error("LLMRouterActivity failed", "error", err) + return nil, err + } + + if output.IsCron { + logger.Info("LLMRouterActivity completed (cron)", + "workflowName", output.CronSpec.Name, + "schedule", output.CronSpec.Schedule, + "stateCount", len(output.CronSpec.States)) + } else { + logger.Info("LLMRouterActivity completed", + "workflowName", output.Spec.Name, + "stateCount", len(output.Spec.States)) + } + + return output, nil +} + +// ValidateWorkflowSpecActivity validates a workflow spec before execution +func ValidateWorkflowSpecActivity(ctx context.Context, spec routing.WorkflowSpec) (*routing.ValidationResult, error) { + logger := activity.GetLogger(ctx) + logger.Info("ValidateWorkflowSpecActivity started", "workflowName", spec.Name) + + kb, err := routing.LoadKnowledgeBaseFromDefaultPath() + if err != nil { + return nil, fmt.Errorf("failed to load knowledge base: %w", err) + } + + validator := routing.NewValidator(kb) + result := validator.ValidateWorkflowSpec(&spec) + + logger.Info("ValidateWorkflowSpecActivity completed", + "valid", result.Valid, + "errorCount", len(result.Errors)) + + return result, nil +} + +// ValidateCronWorkflowSpecActivity validates a cron workflow spec before scheduling +func ValidateCronWorkflowSpecActivity(ctx context.Context, spec routing.CronWorkflowSpec) (*routing.ValidationResult, error) { + logger := activity.GetLogger(ctx) + logger.Info("ValidateCronWorkflowSpecActivity started", + "workflowName", spec.Name, + "schedule", spec.Schedule) + + kb, err := routing.LoadKnowledgeBaseFromDefaultPath() + if err != nil { + return nil, fmt.Errorf("failed to load knowledge base: %w", err) + } + + validator := routing.NewValidator(kb) + result := validator.ValidateCronWorkflowSpec(&spec) + + logger.Info("ValidateCronWorkflowSpecActivity completed", + "valid", result.Valid, + "errorCount", len(result.Errors)) + + return result, nil +} diff --git a/agent-prompts/README.md b/agent-prompts/README.md new file mode 100644 index 0000000..c8a53df --- /dev/null +++ b/agent-prompts/README.md @@ -0,0 +1,59 @@ +# Agent Prompts + +LLM agent prompts for Poimen RoutingWorkflow. + +## Agents + +| Agent | Model | Purpose | Prompt File | +|-------|-------|---------|-------------| +| **Router** | `reasoning` | Natural language β†’ WorkflowSpec | [router/AGENTS.md](router/AGENTS.md) | + +--- + +## Architecture + +``` +User Request: "Scan repo X for security issues" + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ RetrieveMemoryActivity β”‚ +β”‚ Query poimen-memory for relevant skills/lessonsβ”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Router Agent (LLM) β”‚ +β”‚ Input: message + memory context + activities β”‚ +β”‚ Output: WorkflowSpec JSON β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ RoutingWorkflow β”‚ +β”‚ Executes WorkflowSpec as state machine β”‚ +β”‚ Clone β†’ Scan β†’ Report β†’ Notify β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## Memory Integration + +Router receives context from `RetrieveMemoryActivity`: + +``` +1. User: "scan repo for security" +2. RetrieveMemoryActivity queries poimen-memory +3. Returns: skills, lessons, references +4. Injected into Router prompt +5. Router generates smarter WorkflowSpec +``` + +--- + +## LLM Endpoint + +Default: `https://api.riotpiao.com/v1/chat/completions` + +Override: `LOCAL_LLM_BASE_URL=http://localhost:11434` diff --git a/agent-prompts/router/AGENTS.md b/agent-prompts/router/AGENTS.md new file mode 100644 index 0000000..e6b75b3 --- /dev/null +++ b/agent-prompts/router/AGENTS.md @@ -0,0 +1,91 @@ +# Router Agent + +**Purpose**: Intelligent workflow router that analyzes user requests and generates workflow specs. + +**Model**: `reasoning` (via api.riotpiao.com) + +**When Used**: User submits natural language request β†’ Router generates WorkflowSpec/CronWorkflowSpec + +--- + +## System Prompt + +``` +You are an intelligent workflow router. Your job is to: +1. Understand what the user wants to accomplish +2. Select the appropriate activities from the available list +3. Order them correctly based on dependencies +4. Extract any parameters mentioned (URLs, branches, etc) +5. Detect if user wants scheduled/recurring execution +6. Use any relevant knowledge from memory to inform your decisions + +Rules: +- Always include CloneRepoActivity first if any analysis activity is needed +- Order activities respecting dependencies +- If user mentions "daily", "every hour", "weekly", etc β†’ set isCron=true and cronSchedule +- Common cron patterns: "0 2 * * *" (2 AM daily), "0 * * * *" (hourly), "0 0 * * 0" (weekly Sunday) +- Extract repo URLs, branch names, severity levels from the message +- workflowName should be short and descriptive (kebab-case) +- If memory context includes relevant skills or lessons, incorporate that knowledge +- Skills from memory may suggest specific activity parameters or ordering + +Output ONLY valid JSON. +``` + +--- + +## User Prompt Template + +``` +User request: {{.Message}} +{{if .Context}} +Provided context: {{.Context}} +{{end}} +{{if .MemoryContext}} +Relevant skills from memory: +{{range .MemoryContext.Skills}}- {{.Name}}: {{.Description}} (reason: {{.Why}}) +{{end}} +Relevant knowledge from memory: +{{range .MemoryContext.Lessons}}- [{{.Level}}] {{.Text}} +{{end}} +{{end}} +Available activities: +{{range .Activities}}- {{.Name}}: {{.Description}} (category: {{.Category}}, timeout: {{.Timeout}}, flaky: {{.IsFlaky}}) +{{end}} +Analyze the request and output JSON with: +- activities: ordered list of activity names to execute +- parameters: extracted parameters from request (repo URL, branch, etc) +- isCron: true if user wants scheduled/recurring execution +- cronSchedule: cron expression if scheduled (e.g., "0 2 * * *" for 2 AM daily) +- cronTimezone: timezone (default "UTC") +- workflowName: short descriptive name +- errorHandling: "retry" (default), "fail-fast", or "continue" + +Output ONLY valid JSON, no explanation. +``` + +--- + +## Expected Output Format + +```json +{ + "activities": ["CloneRepoActivity", "SecurityScanActivity", "GenerateReportActivity"], + "parameters": { + "repo": "https://github.com/example/repo", + "branch": "main", + "severity": "high" + }, + "isCron": false, + "cronSchedule": "", + "cronTimezone": "UTC", + "workflowName": "security-scan-example", + "errorHandling": "retry" +} +``` + +--- + +## Source File + +`internal/routing/llm_router.go` diff --git a/cmd/starter/main.go b/cmd/starter/main.go index 306d175..a342015 100644 --- a/cmd/starter/main.go +++ b/cmd/starter/main.go @@ -2,9 +2,11 @@ package main import ( "context" + "encoding/json" "flag" "fmt" "log" + "os" "strings" "time" @@ -13,20 +15,27 @@ import ( "github.com/rockliang/poimen/workflows/internal/config" "github.com/rockliang/poimen/workflows/internal/health" "github.com/rockliang/poimen/workflows/internal/logging" + "github.com/rockliang/poimen/workflows/internal/routing" "github.com/rockliang/poimen/workflows/statemachine" ) func main() { var ( + // Orchestrator flags repoPath = flag.String("repo", "", "target repo path") remoteURL = flag.String("remote", "", "remote URL") milestone = flag.String("milestone", "T0", "milestone ID") - dryRun = flag.Bool("dry-run", false, "disable git push/merge") + dryRun = flag.Bool("dry-run", false, "disable git push/merge (orchestrator) or skip submit (routing)") plannerModel = flag.String("planner-model", "reasoning", "planner model ID (local-llm)") judgeModel = flag.String("judge-model", "reasoning", "judge model ID (local-llm)") implementerModel = flag.String("implementer-model", "ornith:35b", "implementer model ID (local-llm ornith)") piProvider = flag.String("pi-provider", "local-llm", "pi provider name for skills (local-llm)") healthCheck = flag.Bool("health", false, "check health and exit") + + // Routing workflow flags + routeMsg = flag.String("route", "", "natural language message for LLM routing") + specFile = flag.String("spec", "", "JSON workflow spec file (direct submit, skip LLM)") + cronSpec = flag.Bool("cron", false, "treat spec as CronWorkflowSpec") ) flag.Parse() @@ -66,9 +75,15 @@ func main() { return } - // Validate required flags for workflow start + // Handle routing workflow mode + if *routeMsg != "" || *specFile != "" { + runRoutingWorkflow(c, *routeMsg, *specFile, *cronSpec, *dryRun) + return + } + + // Validate required flags for orchestrator workflow if *repoPath == "" || *remoteURL == "" { - logging.Fatal("--repo and --remote flags are required") + logging.Fatal("--repo and --remote flags are required (or use --route/--spec for routing workflow)") } @@ -157,3 +172,128 @@ func main() { fmt.Printf("\nWorkflow completed: %+v\n", result) } } + +// runRoutingWorkflow handles --route and --spec flags +func runRoutingWorkflow(c client.Client, routeMsg, specFile string, isCron, dryRun bool) { + ctx := context.Background() + + var spec *routing.WorkflowSpec + var cronSpec *routing.CronWorkflowSpec + + if specFile != "" { + // Load spec from file + data, err := os.ReadFile(specFile) + if err != nil { + logging.Fatal("failed to read spec file", logging.Err(err)) + } + + validator := routing.NewValidator(nil) // nil KB = skip activity validation + if isCron { + cronSpec = &routing.CronWorkflowSpec{} + if err := json.Unmarshal(data, cronSpec); err != nil { + logging.Fatal("failed to parse cron spec", logging.Err(err)) + } + // Validate + result := validator.ValidateCronWorkflowSpec(cronSpec) + if !result.Valid { + logging.Fatal("invalid cron spec", logging.String("errors", result.String())) + } + } else { + spec = &routing.WorkflowSpec{} + if err := json.Unmarshal(data, spec); err != nil { + logging.Fatal("failed to parse spec", logging.Err(err)) + } + // Validate + result := validator.ValidateWorkflowSpec(spec) + if !result.Valid { + logging.Fatal("invalid spec", logging.String("errors", result.String())) + } + } + } else { + // Use LLM router + logging.Info("routing message via LLM", logging.String("message", routeMsg)) + + kb, err := routing.LoadKnowledgeBaseFromDefaultPath() + if err != nil { + logging.Fatal("failed to load knowledge base", logging.Err(err)) + } + + router, err := routing.NewLLMRouter(kb) + if err != nil { + logging.Fatal("failed to create LLM router", logging.Err(err)) + } + + output, err := router.Route(ctx, routing.LLMRouterInput{Message: routeMsg}) + if err != nil { + logging.Fatal("LLM routing failed", logging.Err(err)) + } + + if output.IsCron { + cronSpec = output.CronSpec + fmt.Printf("\n=== Generated Cron Spec ===\n") + fmt.Printf("Name: %s\n", cronSpec.Name) + fmt.Printf("Schedule: %s\n", cronSpec.Schedule) + fmt.Printf("States: %d\n", len(cronSpec.States)) + } else { + spec = output.Spec + fmt.Printf("\n=== Generated Workflow Spec ===\n") + fmt.Printf("Name: %s\n", spec.Name) + fmt.Printf("States: %d\n", len(spec.States)) + } + } + + if dryRun { + fmt.Printf("\n[dry-run] Spec generated but not submitted\n") + if spec != nil { + data, _ := json.MarshalIndent(spec, "", " ") + fmt.Printf("%s\n", data) + } else if cronSpec != nil { + data, _ := json.MarshalIndent(cronSpec, "", " ") + fmt.Printf("%s\n", data) + } + return + } + + // Submit to Temporal + if cronSpec != nil { + // For cron, we'd use Temporal's schedule feature + // For now, just start as regular workflow (cron scheduling TBD) + spec = &routing.WorkflowSpec{ + Name: cronSpec.Name, + Input: cronSpec.Input, + States: cronSpec.States, + } + logging.Warn("cron scheduling not yet implemented, running as one-shot workflow") + } + + workflowID := "routing-" + spec.Name + "-" + time.Now().Format("20060102-150405") + input := statemachine.RoutingWorkflowInput{Spec: spec} + + run, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + ID: workflowID, + TaskQueue: "poimen-taskqueue", + }, statemachine.RoutingWorkflow, input) + if err != nil { + logging.Fatal("failed to start routing workflow", logging.Err(err)) + } + + fmt.Printf("\n=== Routing Workflow Started ===\n") + fmt.Printf("Workflow ID: %s\n", workflowID) + fmt.Printf("Run ID: %s\n", run.GetRunID()) + + // Wait briefly for result + waitCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + var result statemachine.RoutingWorkflowOutput + if err := run.Get(waitCtx, &result); err != nil { + fmt.Printf("\nWorkflow running (check Temporal UI for status)\n") + } else { + fmt.Printf("\nWorkflow completed: %s\n", result.Status) + if len(result.StepResults) > 0 { + for step, res := range result.StepResults { + fmt.Printf(" %s: %v\n", step, res) + } + } + } +} diff --git a/cmd/worker/main.go b/cmd/worker/main.go index 0b525fa..17da652 100644 --- a/cmd/worker/main.go +++ b/cmd/worker/main.go @@ -51,6 +51,7 @@ func main() { w.RegisterWorkflow(statemachine.OrchestratorWorkflow) w.RegisterWorkflow(statemachine.TaskUnitWorkflow) w.RegisterWorkflow(statemachine.TestWorkflow) + w.RegisterWorkflow(statemachine.RoutingWorkflow) // Register all activities w.RegisterActivity(action.CloneRepoActivity) @@ -68,6 +69,25 @@ func main() { // w.RegisterActivity(action.UpdateLessonsActivity) // w.RegisterActivity(action.ReadLessonsActivity) + // Routing workflow activities + w.RegisterActivity(action.LLMRouterActivity) + w.RegisterActivity(action.ValidateWorkflowSpecActivity) + w.RegisterActivity(action.ValidateCronWorkflowSpecActivity) + + // Analysis activities + w.RegisterActivity(action.AnalyzeCodeActivity) + w.RegisterActivity(action.SecurityScanActivity) + w.RegisterActivity(action.GenerateReportActivity) + + // Notification and utility activities + w.RegisterActivity(action.NotifyStatusActivity) + w.RegisterActivity(action.ArchiveResultsActivity) + w.RegisterActivity(action.DeploymentPreCheckActivity) + w.RegisterActivity(action.ApproveWorkflowActivity) + + // Memory activities + w.RegisterActivity(action.RetrieveMemoryActivity) + // Initialize health checker healthChecker := health.NewChecker(c) healthHandler := health.NewHandler(healthChecker) diff --git a/examples/service_integration.go b/examples/service_integration.go new file mode 100644 index 0000000..ec5ff06 --- /dev/null +++ b/examples/service_integration.go @@ -0,0 +1,234 @@ +// Example: External service integrating with Poimen RoutingWorkflow +// Shows how to submit a task and wait for completion + +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "time" + + "go.temporal.io/sdk/client" +) + +// ---- Types (mirror internal/routing/types.go) ---- + +type WorkflowSpec struct { + Name string `json:"name"` + Input map[string]interface{} `json:"input,omitempty"` + States []State `json:"states"` +} + +type State struct { + Name string `json:"name"` + Type string `json:"type"` // Task, Pass, Fail + Resource string `json:"resource,omitempty"` + Parameters map[string]interface{} `json:"parameters,omitempty"` + Timeout string `json:"timeout,omitempty"` + Next string `json:"next,omitempty"` + End bool `json:"end,omitempty"` +} + +type RoutingWorkflowInput struct { + Spec *WorkflowSpec `json:"spec"` +} + +type RoutingWorkflowOutput struct { + Status string `json:"status"` + StepResults map[string]map[string]interface{} `json:"stepResults"` + Error string `json:"error,omitempty"` +} + +// ---- Example Service ---- + +type TaskService struct { + temporalClient client.Client + taskQueue string +} + +func NewTaskService(temporalHost, namespace, taskQueue string) (*TaskService, error) { + c, err := client.Dial(client.Options{ + HostPort: temporalHost, + Namespace: namespace, + }) + if err != nil { + return nil, fmt.Errorf("failed to connect to Temporal: %w", err) + } + + return &TaskService{ + temporalClient: c, + taskQueue: taskQueue, + }, nil +} + +func (s *TaskService) Close() { + s.temporalClient.Close() +} + +// SubmitAndWait submits a workflow spec and waits for completion +func (s *TaskService) SubmitAndWait(ctx context.Context, spec *WorkflowSpec, timeout time.Duration) (*RoutingWorkflowOutput, error) { + workflowID := fmt.Sprintf("%s-%d", spec.Name, time.Now().UnixNano()) + + // Start workflow + run, err := s.temporalClient.ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + ID: workflowID, + TaskQueue: s.taskQueue, + }, "RoutingWorkflow", RoutingWorkflowInput{Spec: spec}) + if err != nil { + return nil, fmt.Errorf("failed to start workflow: %w", err) + } + + log.Printf("Workflow started: ID=%s, RunID=%s", run.GetID(), run.GetRunID()) + + // Wait for completion with timeout + waitCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + var result RoutingWorkflowOutput + if err := run.Get(waitCtx, &result); err != nil { + return nil, fmt.Errorf("workflow failed: %w", err) + } + + return &result, nil +} + +// SubmitAsync submits workflow and returns immediately (fire-and-forget) +func (s *TaskService) SubmitAsync(ctx context.Context, spec *WorkflowSpec) (workflowID string, runID string, err error) { + workflowID = fmt.Sprintf("%s-%d", spec.Name, time.Now().UnixNano()) + + run, err := s.temporalClient.ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + ID: workflowID, + TaskQueue: s.taskQueue, + }, "RoutingWorkflow", RoutingWorkflowInput{Spec: spec}) + if err != nil { + return "", "", fmt.Errorf("failed to start workflow: %w", err) + } + + return run.GetID(), run.GetRunID(), nil +} + +// WaitForCompletion waits for an existing workflow to complete +func (s *TaskService) WaitForCompletion(ctx context.Context, workflowID string, timeout time.Duration) (*RoutingWorkflowOutput, error) { + run := s.temporalClient.GetWorkflow(ctx, workflowID, "") + + waitCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + var result RoutingWorkflowOutput + if err := run.Get(waitCtx, &result); err != nil { + return nil, fmt.Errorf("workflow failed: %w", err) + } + + return &result, nil +} + +// GetStatus gets current workflow status without waiting +func (s *TaskService) GetStatus(ctx context.Context, workflowID string) (string, error) { + desc, err := s.temporalClient.DescribeWorkflowExecution(ctx, workflowID, "") + if err != nil { + return "", err + } + return desc.WorkflowExecutionInfo.Status.String(), nil +} + +// ---- Example Usage ---- + +func main() { + // Connect to Temporal + svc, err := NewTaskService( + "temporal-frontend.temporal:7233", + "poimen-harness", + "poimen-taskqueue", + ) + if err != nil { + log.Fatal(err) + } + defer svc.Close() + + ctx := context.Background() + + // Example 1: Implement task T0.3 with code analysis + fmt.Println("=== Example 1: Submit and Wait ===") + spec := &WorkflowSpec{ + Name: "implement-T0.3", + Input: map[string]interface{}{ + "taskId": "T0.3", + "description": "Implement git worktree management", + "repo": "https://github.com/rockliang/poimen", + }, + States: []State{ + { + Name: "Clone", + Type: "Task", + Resource: "CloneRepoActivity", + Parameters: map[string]interface{}{ + "repo": "${workflow.input.repo}", + "branch": "main", + }, + Timeout: "5m", + Next: "Analyze", + }, + { + Name: "Analyze", + Type: "Task", + Resource: "AnalyzeCodeActivity", + Parameters: map[string]interface{}{ + "path": "${Clone.output.path}", + "depth": 3, + }, + Timeout: "10m", + Next: "Report", + }, + { + Name: "Report", + Type: "Task", + Resource: "GenerateReportActivity", + Parameters: map[string]interface{}{ + "analysisResult": "${Analyze.output}", + "format": "markdown", + }, + Timeout: "2m", + End: true, + }, + }, + } + + result, err := svc.SubmitAndWait(ctx, spec, 30*time.Minute) + if err != nil { + log.Printf("Error: %v", err) + } else { + fmt.Printf("Status: %s\n", result.Status) + for step, output := range result.StepResults { + fmt.Printf(" %s: %v\n", step, output) + } + } + + // Example 2: Fire and forget, then poll + fmt.Println("\n=== Example 2: Async Submit + Poll ===") + workflowID, runID, err := svc.SubmitAsync(ctx, spec) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Submitted: workflowID=%s, runID=%s\n", workflowID, runID) + + // Poll status + for i := 0; i < 5; i++ { + status, _ := svc.GetStatus(ctx, workflowID) + fmt.Printf(" Poll %d: status=%s\n", i+1, status) + if status == "WORKFLOW_EXECUTION_STATUS_COMPLETED" { + break + } + time.Sleep(5 * time.Second) + } + + // Get final result + result, err = svc.WaitForCompletion(ctx, workflowID, 30*time.Minute) + if err != nil { + log.Printf("Error: %v", err) + } else { + resultJSON, _ := json.MarshalIndent(result, "", " ") + fmt.Printf("Final result:\n%s\n", resultJSON) + } +} diff --git a/examples/wait_for_task_test.go b/examples/wait_for_task_test.go new file mode 100644 index 0000000..4440934 --- /dev/null +++ b/examples/wait_for_task_test.go @@ -0,0 +1,122 @@ +// Example: WaitForTaskComplete pattern +// Use case: External service submits implementation task, waits for result + +package main + +import ( + "context" + "fmt" + "time" + + "go.temporal.io/sdk/client" +) + +// WaitForTaskComplete - the core pattern +// +// 1. Build workflow spec for the task +// 2. Submit to Temporal +// 3. Block until completion or timeout +// 4. Return result +func WaitForTaskComplete( + c client.Client, + taskID string, + repo string, + timeout time.Duration, +) (map[string]interface{}, error) { + + ctx := context.Background() + + // Build spec for implementation task + spec := map[string]interface{}{ + "name": fmt.Sprintf("implement-%s", taskID), + "input": map[string]interface{}{ + "taskId": taskID, + "repo": repo, + }, + "states": []map[string]interface{}{ + { + "name": "Clone", + "type": "Task", + "resource": "CloneRepoActivity", + "parameters": map[string]interface{}{ + "repo": repo, + }, + "next": "Analyze", + }, + { + "name": "Analyze", + "type": "Task", + "resource": "AnalyzeCodeActivity", + "parameters": map[string]interface{}{ + "path": "${Clone.output.path}", + }, + "next": "SecurityScan", + }, + { + "name": "SecurityScan", + "type": "Task", + "resource": "SecurityScanActivity", + "parameters": map[string]interface{}{ + "path": "${Clone.output.path}", + }, + "next": "Report", + }, + { + "name": "Report", + "type": "Task", + "resource": "GenerateReportActivity", + "parameters": map[string]interface{}{ + "analysisResult": "${Analyze.output}", + "securityResult": "${SecurityScan.output}", + }, + "end": true, + }, + }, + } + + // Submit workflow + workflowID := fmt.Sprintf("%s-%d", taskID, time.Now().UnixNano()) + run, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + ID: workflowID, + TaskQueue: "poimen-taskqueue", + }, "RoutingWorkflow", map[string]interface{}{"spec": spec}) + if err != nil { + return nil, fmt.Errorf("submit failed: %w", err) + } + + fmt.Printf("[%s] Workflow started: %s\n", taskID, workflowID) + + // Wait for completion + waitCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + var result map[string]interface{} + if err := run.Get(waitCtx, &result); err != nil { + return nil, fmt.Errorf("workflow failed: %w", err) + } + + fmt.Printf("[%s] Workflow completed: %s\n", taskID, result["status"]) + return result, nil +} + +// Example usage in another service: +// +// func (s *MyService) ImplementTask(taskID string) error { +// c, _ := client.Dial(client.Options{ +// HostPort: "temporal-frontend.temporal:7233", +// Namespace: "poimen-harness", +// }) +// defer c.Close() +// +// result, err := WaitForTaskComplete(c, taskID, "https://github.com/...", 30*time.Minute) +// if err != nil { +// return err +// } +// +// // Process result +// if result["status"] == "COMPLETED" { +// report := result["stepResults"].(map[string]interface{})["Report"] +// // Use report... +// } +// return nil +// } diff --git a/go.mod b/go.mod index 88f54e3..8bb42cf 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nexus-rpc/nexus-proto-annotations v0.1.0 // indirect github.com/nexus-rpc/sdk-go v0.7.0 // indirect @@ -26,6 +27,8 @@ require ( github.com/prometheus/common v0.70.1 // indirect github.com/prometheus/procfs v0.21.1 // indirect github.com/robfig/cron v1.2.0 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.9 // indirect github.com/stretchr/objx v0.5.3 // indirect go.temporal.io/api v1.63.4 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/go.sum b/go.sum index dcbe0ad..5f379f4 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,7 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -22,6 +23,8 @@ github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4z github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= @@ -50,6 +53,11 @@ github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= @@ -81,6 +89,7 @@ go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/internal/memory/README.md b/internal/memory/README.md deleted file mode 100644 index f5091a5..0000000 --- a/internal/memory/README.md +++ /dev/null @@ -1,531 +0,0 @@ -# Poimen Memory Service Integration - -Go client for Poimen Memory Service with **Temporal Activities**. Provides create, update, retrieve, and context operations for knowledge management with full workflow integration, retry logic, and observability. - -## Overview - -Memory service endpoints: -- **POST /memory/ingest** β€” Create knowledge records (L1/L2/reference) -- **POST /memory/query** β€” Search knowledge (hybrid semantic+lexical) -- **POST /memory/context** β€” Retrieve context (three-tier: signature β†’ vector β†’ reference) -- **GET /memory/vault** β€” Browse vault files -- **GET /health** β€” Health check - -## Temporal Activities - -All operations are **Temporal Activities** with: -- βœ… Automatic retries (3 attempts by default) -- βœ… Timeout handling (per operation) -- βœ… Heartbeat monitoring -- βœ… Logging + observability -- βœ… Workflow integration - -### Activity List - -| Activity | Purpose | -|----------|---------| -| `CreateKnowledgeActivity` | Create L1/L2/reference records | -| `UpdateKnowledgeActivity` | Update existing knowledge | -| `SearchKnowledgeActivity` | Search hybrid (semantic+lexical) | -| `GetContextActivity` | Retrieve three-tier context | -| `GetVaultActivity` | Browse vault files | -| `HealthCheckActivity` | Check service health | -| `LearnFromExecutionActivity` | Learn from task results | -| `DiagnoseIssueActivity` | Diagnose tool/task issues | -| `AnalyzeErrorActivity` | Analyze errors, find solutions | -| `DocumentDecisionActivity` | Record workflow decisions | -| `SearchAndApplyActivity` | Search and apply knowledge | -| `RefreshMemoryActivity` | Periodic memory refresh | - -### Register Activities - -In worker setup: - -```go -service := memory.NewService(baseURL, token, project) -memory.RegisterMemoryActivities(w, service) -``` - -### Use in Workflows - -```go -// Simple activity call -id, err := memory.ExecuteCreateKnowledge( - ctx, - &memory.KnowledgeRecord{ - Level: "L1", - Content: "...", - }, - nil, // Use default options -) - -// Custom retry policy -options := &memory.ActivityOptions{ - RetryAttempts: 5, - RetryBackoff: time.Second, -} -recommendations, err := memory.ExecuteDiagnoseIssue(ctx, "kubectl", "pod-crash", options) -``` - -## Installation - -Import package: -```go -import "github.com/poimen/workflows/internal/memory" -``` - -## Workflow Integration - -### Example 1: Learning Workflow - -```go -// Learn from task execution -func LearningWorkflow(ctx workflow.Context, taskID string) (string, error) { - // Execute task (placeholder) - result := fmt.Sprintf("Task %s completed successfully", taskID) - - // Learn from result - knowledgeID, err := memory.ExecuteLearnFromExecution( - ctx, - taskID, - result, - []string{"success", taskID}, - nil, // Default retry policy - ) - return knowledgeID, err -} -``` - -### Example 2: Diagnostic Workflow - -```go -// Diagnose issue using memory service -func DiagnosticWorkflow(ctx workflow.Context, tool, issue string) ([]string, error) { - recommendations, err := memory.ExecuteDiagnoseIssue( - ctx, - tool, - issue, - &memory.ActivityOptions{ - RetryAttempts: 3, - RetryBackoff: time.Second, - }, - ) - return recommendations, err -} -``` - -### Example 3: Error Recovery - -```go -// Analyze error and find recovery path -func ErrorRecoveryWorkflow(ctx workflow.Context, errorMsg string) ([]string, error) { - // Analyze error - records, err := memory.ExecuteAnalyzeError(ctx, errorMsg, nil) - if err != nil { - return nil, err - } - - // Extract recovery steps - recovery := make([]string, 0) - for _, record := range records { - if record.Level == "L1" { // High confidence - recovery = append(recovery, record.Content) - } - } - return recovery, nil -} -``` - -### Example 4: Multi-Step Decision Workflow - -```go -// Get context, make decision, document it -func ContextualDecisionWorkflow(ctx workflow.Context, tool, task, decision string) (string, error) { - // Get context (three-tier retrieval) - svcCtx, err := memory.ExecuteGetContext(ctx, tool, task, 8192, nil) - if err != nil { - return "", err - } - - // Make decision based on context - reasoning := fmt.Sprintf("Based on %d lessons (tier %d)", len(svcCtx.Lessons), svcCtx.Tier) - - // Document decision - docID, err := memory.ExecuteDocumentDecision(ctx, tool, decision, reasoning, nil) - return docID, err -} -``` - -## Usage - -### Client (Low-Level) - -```go -package main - -import ( - "context" - "fmt" - "log" - - "github.com/poimen/workflows/internal/memory" -) - -func main() { - // Create client - client := memory.NewClient( - "http://localhost:8080", - "your-jwt-token", - ) - - ctx := context.Background() - - // Ingest knowledge - resp, err := client.Ingest(ctx, &memory.IngestRequest{ - Project: "poimen", - Source: "workflow://task-123", - Kind: "L1", - Text: "Pod CrashLoopBackOff: check logs with kubectl logs", - Metadata: map[string]interface{}{ - "topic": "kubernetes", - "task_id": "debug-pod", - }, - }) - if err != nil { - log.Fatal(err) - } - fmt.Printf("Created: %s (SHA256: %s)\n", resp.ID, resp.SHA256) - - // Search knowledge - query, err := client.Query(ctx, &memory.QueryRequest{ - Project: "poimen", - Query: "fix pod crash loop", - Limit: 5, - Floor: 0.6, // minimum relevance - }) - if err != nil { - log.Fatal(err) - } - for _, r := range query.Results { - fmt.Printf("%s (score: %.2f): %s\n", r.Level, r.Score, r.Text) - } - - // Get context (three-tier retrieval) - ctxResp, err := client.Context(ctx, &memory.ContextRequest{ - Project: "poimen", - Tool: "kubectl", - Task: "debug-pod", - SignatureSource: "error_log", - Budget: 8192, - }) - if err != nil { - log.Fatal(err) - } - fmt.Printf("Context tier: %d\n", ctxResp.Tier) - for _, lesson := range ctxResp.Lessons { - fmt.Printf("- [Tier %d] %s: %.2f\n", lesson.Tier, lesson.Level, lesson.Score) - } - - // Browse vault - vault, err := client.Vault(ctx, "poimen") - if err != nil { - log.Fatal(err) - } - fmt.Printf("Total records: %d\n", vault.TotalRecords) - for _, f := range vault.Files { - fmt.Printf("- %s (%s, %d records)\n", f.Path, f.Level, f.RecordCount) - } -} -``` - -### Service (High-Level) - -```go -package main - -import ( - "context" - "log" - - "github.com/poimen/workflows/internal/memory" -) - -func main() { - // Create service - svc := memory.NewService( - "http://localhost:8080", - "your-jwt-token", - "poimen", // project - ) - - ctx := context.Background() - - // Create knowledge - id, err := svc.CreateKnowledge(ctx, &memory.KnowledgeRecord{ - Level: "L1", - Title: "Pod Debugging", - Content: "To debug CrashLoopBackOff: kubectl logs ", - Source: "workflow://debug-task", - }) - if err != nil { - log.Fatal(err) - } - log.Printf("Created knowledge: %s\n", id) - - // Update knowledge (re-ingest with same ID) - id, err = svc.UpdateKnowledge(ctx, &memory.KnowledgeRecord{ - ID: id, - Level: "L2", - Content: "Advanced debugging: check events, describe pod, check node status", - }) - if err != nil { - log.Fatal(err) - } - log.Printf("Updated knowledge: %s\n", id) - - // Retrieve knowledge - records, err := svc.RetrieveKnowledge(ctx, "kubernetes pod debugging", &memory.RetrievalOptions{ - LevelFilter: []string{"L1", "L2"}, - Limit: 10, - Floor: 0.7, - }) - if err != nil { - log.Fatal(err) - } - for _, rec := range records { - log.Printf("- %s: %s\n", rec.ID, rec.Content) - } - - // Retrieve context - svcCtx, err := svc.RetrieveContext(ctx, "kubectl", "debug-pod", 8192) - if err != nil { - log.Fatal(err) - } - log.Printf("Context tier: %d (%d lessons, %d skills)\n", - svcCtx.Tier, len(svcCtx.Lessons), len(svcCtx.Skills)) - for _, skill := range svcCtx.Skills { - log.Printf(" - %s: %s\n", skill.Name, skill.Why) - } - - // Get vault - files, err := svc.GetVault(ctx) - if err != nil { - log.Fatal(err) - } - log.Printf("Vault has %d files\n", len(files)) - - // Check health - if svc.IsHealthy(ctx) { - log.Println("Memory service is healthy") - } -} -``` - -## API Reference - -### Client Methods - -#### Ingest(ctx, req) β†’ IngestResponse, error -Create knowledge record. - -Request: -```go -&IngestRequest{ - Project: "poimen", - Source: "workflow://task-id", - Kind: "L1", // L1|L2|reference - Text: "knowledge content", - Metadata: map[string]interface{}{...}, -} -``` - -Response: -```go -{ - ID: "chunk-abc123", - SHA256: "de12cd34ef56...", - QueueStatus: "pending", // Async processing - IdempotencyID: "sess-123:0", -} -``` - -#### Query(ctx, req) β†’ QueryResponse, error -Search knowledge (hybrid semantic + lexical). - -Request: -```go -&QueryRequest{ - Project: "poimen", - Query: "fix kubernetes pod crash", - LevelFilter: []string{"L1", "L2"}, // Optional - Floor: 0.6, // Minimum relevance - Limit: 10, - Scope: "all", // learned|reference|all -} -``` - -Response: -```go -{ - Query: "...", - Results: []QueryResult{ - { - ID: "chunk-abc123", - Level: "L1", - Score: 0.992, - SemanticScore: 1.0, - LexicalScore: 0.98, - Text: "...", - Breadcrumb: "kubernetes.md > Troubleshooting", - Source: "transcript://session-123", - }, - ... - }, - TotalHits: 127, - SearchTimeMS: 145, -} -``` - -#### Context(ctx, req) β†’ ContextResponse, error -Retrieve context for tool/task (three-tier retrieval: signature β†’ vector β†’ reference). - -Request: -```go -&ContextRequest{ - Project: "poimen", - Tool: "kubectl", - Task: "debug-pod", - SignatureSource: "failure_log", // Where to find signature - Scope: "tool_context", - Budget: 8192, // Max response bytes -} -``` - -Response: -```go -{ - Tier: 1, // Highest tier with results - Lessons: []ContextLesson{ - { - Tier: 1, - Level: "L1", - Score: 1.0, - Text: "Pod in CrashLoopBackOff: check logs", - MatchedKind: "signature", - SeenCount: 23, - LastSeen: "2025-01-28T15:30:00Z", - }, - ... - }, - Skills: []ContextSkill{ - { - Name: "diagnose-pod-failure", - Why: "Tier-1 signature matched", - }, - }, - Budget: { - Requested: 8192, - Used: 4156, - Dropped: 0, - Degradation: nil, - }, -} -``` - -#### Vault(ctx, project) β†’ VaultResponse, error -Browse vault files. - -Response: -```go -{ - Project: "poimen", - Files: []VaultFile{ - { - Path: "kubernetes/debugging.md", - Title: "Debugging", - Level: "L1", - UpdatedAt: "2025-01-28T10:00:00Z", - RecordCount: 23, - }, - ... - }, - TotalRecords: 542, -} -``` - -#### Health(ctx) β†’ bool, error -Check service health. - -### Service Methods - -Service provides higher-level operations: - -- `CreateKnowledge(ctx, record)` β†’ id, error -- `UpdateKnowledge(ctx, record)` β†’ id, error -- `RetrieveKnowledge(ctx, query, opts)` β†’ []KnowledgeRecord, error -- `RetrieveContext(ctx, tool, task, budget)` β†’ *ServiceContext, error -- `GetVault(ctx)` β†’ []VaultInfo, error -- `IsHealthy(ctx)` β†’ bool - -## Error Handling - -```go -// All operations return (result, error) -resp, err := client.Ingest(ctx, req) -if err != nil { - // Possible errors: - // - Request marshal/network errors - // - 401 Unauthorized: Missing/invalid JWT - // - 403 Forbidden: Token lacks capability - // - 429 Too Many Requests: Rate limit exceeded - // - 409 Conflict: Duplicate (same idempotency key within 24h) - // - 503 Service Unavailable: Database unreachable - log.Fatalf("ingest failed: %v", err) -} -``` - -## Authentication - -Pass JWT bearer token to NewClient/NewService: - -```go -// Get token from Authentik -token := "eyJ0eXAiOiJKV1QiLCJhbGc..." - -client := memory.NewClient(baseURL, token) -``` - -Token must have capability: -- `memory:read` β€” for Query, Context, Vault -- `memory:write` β€” for Ingest - -## Rate Limits - -Per JWT identity: -- Ingest: 100/hour -- Query: 1000/hour -- Context: 100/hour - -Exceed limit β†’ 429 Too Many Requests. - -## Deployment - -Memory service endpoints (k8s): -- Service: `memory-service.poimen.svc.cluster.local:8080` -- Ingress: `https://memory.riotpiao.com` (external) - -Environment: -```go -baseURL := "http://memory-service.poimen.svc.cluster.local:8080" -token := os.Getenv("MEMORY_SERVICE_TOKEN") -svc := memory.NewService(baseURL, token, "poimen") -``` - -## Testing - -Run tests: -```bash -go test ./internal/memory -v -``` - -Mock server example in `client_test.go` and `service_test.go`. diff --git a/internal/routing/activity_knowledge_base.json b/internal/routing/activity_knowledge_base.json index b065a8c..884d473 100644 --- a/internal/routing/activity_knowledge_base.json +++ b/internal/routing/activity_knowledge_base.json @@ -341,10 +341,76 @@ "dependencies": [], "notes": "Network-dependent. May fail on network issues or service throttling. Retry 2x." } + }, + { + "name": "RetrieveMemoryActivity", + "description": "Retrieve relevant knowledge, skills, and lessons from poimen-memory semantic search", + "category": "memory", + "inputs": { + "query": { + "type": "string", + "description": "Semantic search query", + "required": true + }, + "project": { + "type": "string", + "description": "Memory project (default: poimen)", + "required": false, + "default": "poimen" + }, + "scope": { + "type": "string", + "description": "Retrieval scope: skills, lessons, references, all", + "required": false, + "default": "all" + }, + "limit": { + "type": "integer", + "description": "Max results to return", + "required": false, + "default": 10 + }, + "tool": { + "type": "string", + "description": "Tool context for skill matching", + "required": false + }, + "task": { + "type": "string", + "description": "Task description for context retrieval", + "required": false + } + }, + "outputs": { + "skills": { + "type": "array", + "description": "Relevant skills found" + }, + "lessons": { + "type": "array", + "description": "Relevant lessons/knowledge found" + }, + "references": { + "type": "array", + "description": "Reference documents found" + }, + "totalResults": { + "type": "integer", + "description": "Total results found" + } + }, + "constraints": { + "defaultTimeout": "30s", + "isFlaky": true, + "recommendedRetries": 2, + "retryBackoff": 1.5, + "dependencies": [], + "notes": "Network-dependent. First activity to run for context-aware routing. Fast timeout." + } } ], "metadata": { - "totalActivities": 8, + "totalActivities": 9, "lastUpdated": "2025-08-31T00:00:00Z", "categories": { "repository": 1, @@ -354,7 +420,8 @@ "deployment": 1, "notification": 1, "approval": 1, - "storage": 1 + "storage": 1, + "memory": 1 } } } diff --git a/internal/routing/knowledge_base.go b/internal/routing/knowledge_base.go index b352860..195ed95 100644 --- a/internal/routing/knowledge_base.go +++ b/internal/routing/knowledge_base.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "path/filepath" + "runtime" ) // KnowledgeBase represents the activity knowledge base @@ -72,6 +73,21 @@ func LoadKnowledgeBaseFromDefaultPath() (*KnowledgeBase, error) { return LoadKnowledgeBase("internal/routing/activity_knowledge_base.json") } + // Try from parent directory (for tests running from tests/ dir) + if _, err := os.Stat("../internal/routing/activity_knowledge_base.json"); err == nil { + return LoadKnowledgeBase("../internal/routing/activity_knowledge_base.json") + } + + // Try using runtime to find package directory + _, filename, _, ok := runtime.Caller(0) + if ok { + pkgDir := filepath.Dir(filename) + path := filepath.Join(pkgDir, "activity_knowledge_base.json") + if _, err := os.Stat(path); err == nil { + return LoadKnowledgeBase(path) + } + } + return nil, fmt.Errorf("activity_knowledge_base.json not found in any expected location") } diff --git a/internal/routing/llm_client.go b/internal/routing/llm_client.go new file mode 100644 index 0000000..5819f76 --- /dev/null +++ b/internal/routing/llm_client.go @@ -0,0 +1,110 @@ +package routing + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" +) + +var ( + // llmBaseURL is the base URL for the LLM API + llmBaseURL string +) + +func init() { + llmBaseURL = os.Getenv("LOCAL_LLM_BASE_URL") + if llmBaseURL == "" { + llmBaseURL = "https://api.riotpiao.com" + } +} + +// LLMClient is a simple LLM client for routing +type LLMClient struct { + baseURL string + httpClient *http.Client +} + +// NewLLMClient creates a new LLM client +func NewLLMClient() *LLMClient { + return &LLMClient{ + baseURL: llmBaseURL, + httpClient: &http.Client{}, + } +} + +// llmRequest is the request body for the OpenAI-compatible API +type llmRequest struct { + Model string `json:"model"` + Messages []llmMessage `json:"messages"` + Stream bool `json:"stream"` +} + +type llmMessage struct { + Role string `json:"role"` + Content string `json:"content"` +} + +// llmResponse is the response from the OpenAI-compatible API +type llmResponse struct { + Choices []struct { + Message struct { + Content string `json:"content"` + } `json:"message"` + } `json:"choices"` +} + +// Chat sends a chat completion request +func (c *LLMClient) Chat(ctx context.Context, systemPrompt, userMessage string) (string, error) { + req := llmRequest{ + Model: "reasoning", + Messages: []llmMessage{ + {Role: "system", Content: systemPrompt}, + {Role: "user", Content: userMessage}, + }, + Stream: false, + } + + reqBody, err := json.Marshal(req) + if err != nil { + return "", fmt.Errorf("failed to marshal request: %w", err) + } + + httpReq, err := http.NewRequestWithContext(ctx, "POST", + fmt.Sprintf("%s/v1/chat/completions", c.baseURL), + bytes.NewReader(reqBody)) + if err != nil { + return "", fmt.Errorf("failed to create HTTP request: %w", err) + } + + httpReq.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(httpReq) + if err != nil { + return "", fmt.Errorf("failed to connect to LLM API at %s: %w", c.baseURL, err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response body: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("LLM API returned status %d: %s", resp.StatusCode, string(respBody)) + } + + var respObj llmResponse + if err := json.Unmarshal(respBody, &respObj); err != nil { + return "", fmt.Errorf("failed to unmarshal response: %w", err) + } + + if len(respObj.Choices) == 0 { + return "", fmt.Errorf("no choices in response from LLM API") + } + + return respObj.Choices[0].Message.Content, nil +} diff --git a/internal/routing/llm_router.go b/internal/routing/llm_router.go new file mode 100644 index 0000000..12fe6e3 --- /dev/null +++ b/internal/routing/llm_router.go @@ -0,0 +1,456 @@ +package routing + +import ( + "context" + "encoding/json" + "fmt" + "regexp" + "strings" +) + +// LLMRouterInput is input to the llm-router activity +type LLMRouterInput struct { + Message string `json:"message"` + Context map[string]interface{} `json:"context,omitempty"` // Optional context (repo, branch, etc) + MemoryContext *MemoryContext `json:"memoryContext,omitempty"` // Optional memory retrieval results + UseMemory bool `json:"useMemory,omitempty"` // Enable memory retrieval (default: false) +} + +// MemoryContext holds retrieved memory for prompt injection +type MemoryContext struct { + Skills []MemorySkill `json:"skills"` + Lessons []MemoryLesson `json:"lessons"` + References []MemoryReference `json:"references"` +} + +// MemorySkill from memory service +type MemorySkill struct { + Name string `json:"name"` + Description string `json:"description"` + Why string `json:"why,omitempty"` +} + +// MemoryLesson from memory service +type MemoryLesson struct { + ID string `json:"id"` + Text string `json:"text"` + Level string `json:"level"` +} + +// MemoryReference from memory service +type MemoryReference struct { + ID string `json:"id"` + Text string `json:"text"` +} + +// LLMRouterOutput is output from the llm-router activity +type LLMRouterOutput struct { + Spec *WorkflowSpec `json:"spec,omitempty"` + CronSpec *CronWorkflowSpec `json:"cronSpec,omitempty"` + IsCron bool `json:"isCron"` + Error string `json:"error,omitempty"` +} + +// LLMRouter orchestrates intent analysis and spec generation +type LLMRouter struct { + client *LLMClient + knowledgeBase *KnowledgeBase +} + +// NewLLMRouter creates a new LLM router +func NewLLMRouter(kb *KnowledgeBase) (*LLMRouter, error) { + return &LLMRouter{ + client: NewLLMClient(), + knowledgeBase: kb, + }, nil +} + +// Route analyzes user message and generates appropriate workflow spec +func (r *LLMRouter) Route(ctx context.Context, input LLMRouterInput) (*LLMRouterOutput, error) { + // 1. Analyze intent using LLM + intent, err := r.analyzeIntent(ctx, input) + if err != nil { + return nil, fmt.Errorf("intent analysis failed: %w", err) + } + + // 2. Build workflow spec based on intent + if intent.IsCron { + cronSpec, err := r.buildCronSpec(intent, input) + if err != nil { + return nil, fmt.Errorf("cron spec build failed: %w", err) + } + return &LLMRouterOutput{ + CronSpec: cronSpec, + IsCron: true, + }, nil + } + + spec, err := r.buildSpec(intent, input) + if err != nil { + return nil, fmt.Errorf("spec build failed: %w", err) + } + + return &LLMRouterOutput{ + Spec: spec, + IsCron: false, + }, nil +} + +// Intent represents analyzed user intent +type Intent struct { + Activities []string `json:"activities"` // Selected activity names + Parameters map[string]interface{} `json:"parameters"` // Extracted parameters + IsCron bool `json:"isCron"` // Is scheduled workflow? + CronSchedule string `json:"cronSchedule"` // Cron expression if scheduled + CronTimezone string `json:"cronTimezone"` // Timezone for cron + WorkflowName string `json:"workflowName"` // Generated workflow name + ErrorHandling string `json:"errorHandling"` // "retry", "fail-fast", "continue" +} + +// analyzeIntent uses LLM to understand user request +func (r *LLMRouter) analyzeIntent(ctx context.Context, input LLMRouterInput) (*Intent, error) { + // Build prompt with knowledge base context + prompt := r.buildIntentPrompt(input) + + // Call LLM + response, err := r.client.Chat(ctx, intentSystemPrompt, prompt) + if err != nil { + return nil, fmt.Errorf("LLM call failed: %w", err) + } + + // Parse LLM response + intent, err := parseIntentResponse(response) + if err != nil { + return nil, fmt.Errorf("failed to parse intent: %w", err) + } + + // Validate activities exist + for _, actName := range intent.Activities { + if !r.knowledgeBase.HasActivity(actName) { + return nil, fmt.Errorf("unknown activity: %s", actName) + } + } + + return intent, nil +} + +// buildIntentPrompt creates the prompt for intent analysis +func (r *LLMRouter) buildIntentPrompt(input LLMRouterInput) string { + // Get activity summaries + var activityList strings.Builder + for _, act := range r.knowledgeBase.Activities { + activityList.WriteString(fmt.Sprintf("- %s: %s (category: %s, timeout: %s, flaky: %v)\n", + act.Name, act.Description, act.Category, + act.Constraints.DefaultTimeout, act.Constraints.IsFlaky)) + } + + // Build context string + contextStr := "" + if len(input.Context) > 0 { + ctxBytes, _ := json.Marshal(input.Context) + contextStr = fmt.Sprintf("\nProvided context: %s", string(ctxBytes)) + } + + // Build memory context string + memoryStr := r.formatMemoryContext(input.MemoryContext) + + return fmt.Sprintf(`User request: %s +%s%s +Available activities: +%s +Analyze the request and output JSON with: +- activities: ordered list of activity names to execute +- parameters: extracted parameters from request (repo URL, branch, etc) +- isCron: true if user wants scheduled/recurring execution +- cronSchedule: cron expression if scheduled (e.g., "0 2 * * *" for 2 AM daily) +- cronTimezone: timezone (default "UTC") +- workflowName: short descriptive name +- errorHandling: "retry" (default), "fail-fast", or "continue" + +Output ONLY valid JSON, no explanation.`, input.Message, contextStr, memoryStr, activityList.String()) +} + +// formatMemoryContext formats memory context for prompt injection +func (r *LLMRouter) formatMemoryContext(mem *MemoryContext) string { + if mem == nil { + return "" + } + + var sb strings.Builder + + if len(mem.Skills) > 0 { + sb.WriteString("\n\nRelevant skills from memory:\n") + for _, skill := range mem.Skills { + if skill.Why != "" { + sb.WriteString(fmt.Sprintf("- %s: %s (reason: %s)\n", skill.Name, skill.Description, skill.Why)) + } else { + sb.WriteString(fmt.Sprintf("- %s: %s\n", skill.Name, skill.Description)) + } + } + } + + if len(mem.Lessons) > 0 { + sb.WriteString("\nRelevant knowledge from memory:\n") + for _, lesson := range mem.Lessons { + text := lesson.Text + if len(text) > 300 { + text = text[:300] + "..." + } + sb.WriteString(fmt.Sprintf("- [%s] %s\n", lesson.Level, text)) + } + } + + if len(mem.References) > 0 { + sb.WriteString("\nReference documents:\n") + for _, ref := range mem.References { + text := ref.Text + if len(text) > 200 { + text = text[:200] + "..." + } + sb.WriteString(fmt.Sprintf("- %s\n", text)) + } + } + + return sb.String() +} + +// parseIntentResponse extracts Intent from LLM response +func parseIntentResponse(response string) (*Intent, error) { + // Try to extract JSON from response + response = strings.TrimSpace(response) + + // Handle markdown code blocks + if strings.HasPrefix(response, "```") { + re := regexp.MustCompile("```(?:json)?\\s*([\\s\\S]*?)```") + matches := re.FindStringSubmatch(response) + if len(matches) > 1 { + response = strings.TrimSpace(matches[1]) + } + } + + var intent Intent + if err := json.Unmarshal([]byte(response), &intent); err != nil { + return nil, fmt.Errorf("invalid JSON from LLM: %w\nResponse: %s", err, response) + } + + // Set defaults + if intent.CronTimezone == "" { + intent.CronTimezone = "UTC" + } + if intent.ErrorHandling == "" { + intent.ErrorHandling = "retry" + } + if intent.WorkflowName == "" { + intent.WorkflowName = "generated-workflow" + } + + return &intent, nil +} + +// buildSpec creates WorkflowSpec from intent +func (r *LLMRouter) buildSpec(intent *Intent, input LLMRouterInput) (*WorkflowSpec, error) { + if len(intent.Activities) == 0 { + return nil, fmt.Errorf("no activities selected") + } + + states := make([]State, 0, len(intent.Activities)+1) + + // Build states for each activity + for i, actName := range intent.Activities { + act := r.knowledgeBase.GetActivity(actName) + + state := State{ + Name: actName, + Type: StateTypeTask, + Resource: actName, + Parameters: r.buildParameters(act, intent, i), + Timeout: act.Constraints.DefaultTimeout, + Retry: r.buildRetryPolicy(act, intent), + } + + // Set next state or end + if i < len(intent.Activities)-1 { + state.Next = intent.Activities[i+1] + } else { + state.End = true + } + + // Add catch clause for flaky activities + if act.Constraints.IsFlaky && intent.ErrorHandling != "fail-fast" { + state.Catch = []CatchClause{ + { + ErrorEquals: []string{"ActivityError", "TimeoutError"}, + Next: "HandleError", + }, + } + } + + states = append(states, state) + } + + // Add error handler if needed + hasFlaky := false + for _, actName := range intent.Activities { + act := r.knowledgeBase.GetActivity(actName) + if act != nil && act.Constraints.IsFlaky { + hasFlaky = true + break + } + } + if hasFlaky && intent.ErrorHandling != "fail-fast" { + states = append(states, State{ + Name: "HandleError", + Type: StateTypeFail, + Error: "WorkflowError", + Cause: "Activity failed after retries", + }) + } + + // Build input map + inputMap := make(map[string]interface{}) + for k, v := range intent.Parameters { + inputMap[k] = v + } + for k, v := range input.Context { + if _, exists := inputMap[k]; !exists { + inputMap[k] = v + } + } + + return &WorkflowSpec{ + Name: intent.WorkflowName, + Input: inputMap, + States: states, + }, nil +} + +// buildCronSpec creates CronWorkflowSpec from intent +func (r *LLMRouter) buildCronSpec(intent *Intent, input LLMRouterInput) (*CronWorkflowSpec, error) { + // First build regular spec + spec, err := r.buildSpec(intent, input) + if err != nil { + return nil, err + } + + // Get schedule - check both intent and parameters (LLM sometimes puts it in parameters) + schedule := intent.CronSchedule + if schedule == "" { + if sched, ok := intent.Parameters["cronSchedule"].(string); ok { + schedule = sched + } + } + if schedule == "" { + if sched, ok := spec.Input["cronSchedule"].(string); ok { + schedule = sched + delete(spec.Input, "cronSchedule") // Remove from input + } + } + + // Get timezone + timezone := intent.CronTimezone + if timezone == "" { + if tz, ok := intent.Parameters["cronTimezone"].(string); ok { + timezone = tz + } + } + if timezone == "" { + if tz, ok := spec.Input["cronTimezone"].(string); ok { + timezone = tz + delete(spec.Input, "cronTimezone") // Remove from input + } + } + if timezone == "" { + timezone = "UTC" + } + + return &CronWorkflowSpec{ + Name: spec.Name, + Type: "CronWorkflow", + Schedule: schedule, + Timezone: timezone, + Input: spec.Input, + States: spec.States, + MaxConcurrent: 1, + Timeout: "1h", + EnableHistory: true, + }, nil +} + +// buildParameters creates parameter map for activity +func (r *LLMRouter) buildParameters(act *ActivityMetadata, intent *Intent, stateIndex int) map[string]interface{} { + params := make(map[string]interface{}) + + for inputName, inputDef := range act.Inputs { + // Check if parameter was extracted from intent + if val, ok := intent.Parameters[inputName]; ok { + params[inputName] = val + continue + } + + // Check for JSONPath reference from previous state + if stateIndex > 0 { + prevAct := intent.Activities[stateIndex-1] + prevActDef := r.knowledgeBase.GetActivity(prevAct) + + // Look for matching output from previous activity + for outName := range prevActDef.Outputs { + if outName == inputName || strings.EqualFold(outName, inputName) { + params[inputName] = fmt.Sprintf("${%s.output.%s}", prevAct, outName) + break + } + } + } + + // Use default if available + if params[inputName] == nil && inputDef.Default != nil { + params[inputName] = inputDef.Default + } + + // Use input reference for common fields + if params[inputName] == nil { + if inputName == "repo" || inputName == "path" || inputName == "branch" { + params[inputName] = fmt.Sprintf("${input.%s}", inputName) + } + } + } + + return params +} + +// buildRetryPolicy creates retry policy based on activity constraints +func (r *LLMRouter) buildRetryPolicy(act *ActivityMetadata, intent *Intent) *RetryPolicy { + if intent.ErrorHandling == "fail-fast" { + return &RetryPolicy{ + MaxAttempts: 1, + BackoffRate: 1.0, + InitialInterval: "1s", + } + } + + return &RetryPolicy{ + MaxAttempts: int32(act.Constraints.RecommendedRetries), + BackoffRate: act.Constraints.RetryBackoff, + InitialInterval: "1s", + MaxInterval: "30s", + } +} + +const intentSystemPrompt = `You are an intelligent workflow router. Your job is to: +1. Understand what the user wants to accomplish +2. Select the appropriate activities from the available list +3. Order them correctly based on dependencies +4. Extract any parameters mentioned (URLs, branches, etc) +5. Detect if user wants scheduled/recurring execution +6. Use any relevant knowledge from memory to inform your decisions + +Rules: +- Always include CloneRepoActivity first if any analysis activity is needed +- Order activities respecting dependencies +- If user mentions "daily", "every hour", "weekly", etc β†’ set isCron=true and cronSchedule +- Common cron patterns: "0 2 * * *" (2 AM daily), "0 * * * *" (hourly), "0 0 * * 0" (weekly Sunday) +- Extract repo URLs, branch names, severity levels from the message +- workflowName should be short and descriptive (kebab-case) +- If memory context includes relevant skills or lessons, incorporate that knowledge +- Skills from memory may suggest specific activity parameters or ordering + +Output ONLY valid JSON.` diff --git a/internal/routing/llm_router_integration_test.go b/internal/routing/llm_router_integration_test.go new file mode 100644 index 0000000..ec33727 --- /dev/null +++ b/internal/routing/llm_router_integration_test.go @@ -0,0 +1,103 @@ +// +build integration + +package routing + +import ( + "context" + "encoding/json" + "os" + "testing" + "time" +) + +// TestLLMRouterIntegration tests against real api.riotpiao.com +// Run with: go test -tags=integration -v -run TestLLMRouterIntegration +func TestLLMRouterIntegration(t *testing.T) { + // Skip if not explicitly enabled + if os.Getenv("RUN_INTEGRATION_TESTS") != "1" { + t.Skip("Skipping integration test. Set RUN_INTEGRATION_TESTS=1 to run.") + } + + kb, err := LoadKnowledgeBaseFromDefaultPath() + if err != nil { + t.Fatalf("failed to load knowledge base: %v", err) + } + + router, err := NewLLMRouter(kb) + if err != nil { + t.Fatalf("failed to create router: %v", err) + } + + tests := []struct { + name string + input LLMRouterInput + validate func(*testing.T, *LLMRouterOutput) + }{ + { + name: "analyze repo request", + input: LLMRouterInput{ + Message: "Analyze the GitHub repo https://github.com/rockliang/poimen for code quality and security issues", + Context: map[string]interface{}{ + "branch": "main", + }, + }, + validate: func(t *testing.T, output *LLMRouterOutput) { + if output.IsCron { + t.Error("expected one-time workflow, not cron") + } + if output.Spec == nil { + t.Fatal("expected spec, got nil") + } + if len(output.Spec.States) < 2 { + t.Errorf("expected at least 2 states, got %d", len(output.Spec.States)) + } + // Should start with CloneRepoActivity + if output.Spec.States[0].Resource != "CloneRepoActivity" { + t.Errorf("expected first activity to be CloneRepoActivity, got %s", output.Spec.States[0].Resource) + } + t.Logf("Generated workflow: %s with %d states", output.Spec.Name, len(output.Spec.States)) + for i, state := range output.Spec.States { + t.Logf(" State %d: %s (%s)", i, state.Name, state.Resource) + } + }, + }, + { + name: "daily security scan (cron)", + input: LLMRouterInput{ + Message: "Run a security scan on https://github.com/rockliang/poimen every day at 3 AM UTC", + }, + validate: func(t *testing.T, output *LLMRouterOutput) { + if !output.IsCron { + t.Error("expected cron workflow") + } + if output.CronSpec == nil { + t.Fatal("expected cron spec, got nil") + } + if output.CronSpec.Schedule == "" { + t.Error("expected cron schedule") + } + t.Logf("Generated cron workflow: %s, schedule: %s", output.CronSpec.Name, output.CronSpec.Schedule) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + output, err := router.Route(ctx, tt.input) + if err != nil { + t.Fatalf("Route failed: %v", err) + } + + // Pretty print output + jsonOut, _ := json.MarshalIndent(output, "", " ") + t.Logf("Output:\n%s", string(jsonOut)) + + if tt.validate != nil { + tt.validate(t, output) + } + }) + } +} diff --git a/internal/routing/llm_router_test.go b/internal/routing/llm_router_test.go new file mode 100644 index 0000000..ce6b5a1 --- /dev/null +++ b/internal/routing/llm_router_test.go @@ -0,0 +1,305 @@ +package routing + +import ( + "encoding/json" + "testing" +) + +func TestParseIntentResponse(t *testing.T) { + tests := []struct { + name string + response string + wantErr bool + validate func(*testing.T, *Intent) + }{ + { + name: "basic intent", + response: `{ + "activities": ["CloneRepoActivity", "AnalyzeCodeActivity"], + "parameters": {"repo": "https://github.com/test/repo"}, + "isCron": false, + "workflowName": "analyze-repo" + }`, + wantErr: false, + validate: func(t *testing.T, intent *Intent) { + if len(intent.Activities) != 2 { + t.Errorf("expected 2 activities, got %d", len(intent.Activities)) + } + if intent.Activities[0] != "CloneRepoActivity" { + t.Errorf("expected CloneRepoActivity first, got %s", intent.Activities[0]) + } + if intent.IsCron { + t.Error("expected isCron=false") + } + }, + }, + { + name: "cron intent", + response: `{ + "activities": ["CloneRepoActivity", "SecurityScanActivity"], + "parameters": {"repo": "https://github.com/test/repo"}, + "isCron": true, + "cronSchedule": "0 2 * * *", + "cronTimezone": "America/New_York", + "workflowName": "daily-security-scan" + }`, + wantErr: false, + validate: func(t *testing.T, intent *Intent) { + if !intent.IsCron { + t.Error("expected isCron=true") + } + if intent.CronSchedule != "0 2 * * *" { + t.Errorf("expected cron schedule '0 2 * * *', got %s", intent.CronSchedule) + } + if intent.CronTimezone != "America/New_York" { + t.Errorf("expected timezone 'America/New_York', got %s", intent.CronTimezone) + } + }, + }, + { + name: "with markdown code block", + response: "```json\n{\"activities\": [\"CloneRepoActivity\"], \"parameters\": {}, \"isCron\": false}\n```", + wantErr: false, + validate: func(t *testing.T, intent *Intent) { + if len(intent.Activities) != 1 { + t.Errorf("expected 1 activity, got %d", len(intent.Activities)) + } + }, + }, + { + name: "defaults applied", + response: `{"activities": ["CloneRepoActivity"], "parameters": {}}`, + wantErr: false, + validate: func(t *testing.T, intent *Intent) { + if intent.CronTimezone != "UTC" { + t.Errorf("expected default timezone UTC, got %s", intent.CronTimezone) + } + if intent.ErrorHandling != "retry" { + t.Errorf("expected default errorHandling 'retry', got %s", intent.ErrorHandling) + } + if intent.WorkflowName != "generated-workflow" { + t.Errorf("expected default workflowName, got %s", intent.WorkflowName) + } + }, + }, + { + name: "invalid json", + response: "this is not json", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + intent, err := parseIntentResponse(tt.response) + if tt.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + if tt.validate != nil { + tt.validate(t, intent) + } + }) + } +} + +func TestBuildSpec(t *testing.T) { + // Load knowledge base + kb, err := LoadKnowledgeBaseFromDefaultPath() + if err != nil { + t.Fatalf("failed to load knowledge base: %v", err) + } + + router := &LLMRouter{ + knowledgeBase: kb, + } + + intent := &Intent{ + Activities: []string{"CloneRepoActivity", "AnalyzeCodeActivity", "SecurityScanActivity"}, + Parameters: map[string]interface{}{"repo": "https://github.com/test/repo", "branch": "main"}, + WorkflowName: "test-workflow", + ErrorHandling: "retry", + } + + input := LLMRouterInput{ + Message: "Analyze repo for security", + Context: map[string]interface{}{}, + } + + spec, err := router.buildSpec(intent, input) + if err != nil { + t.Fatalf("buildSpec failed: %v", err) + } + + // Validate spec + if spec.Name != "test-workflow" { + t.Errorf("expected name 'test-workflow', got %s", spec.Name) + } + + if len(spec.States) < 3 { + t.Errorf("expected at least 3 states, got %d", len(spec.States)) + } + + // First state should be CloneRepoActivity + if spec.States[0].Resource != "CloneRepoActivity" { + t.Errorf("expected first state to be CloneRepoActivity, got %s", spec.States[0].Resource) + } + + // Last activity state should have End=true + lastActivityIdx := len(spec.States) - 1 + if spec.States[lastActivityIdx].Type == StateTypeFail { + lastActivityIdx-- + } + if !spec.States[lastActivityIdx].End { + t.Error("expected last activity state to have End=true") + } + + // Check retry policy on flaky activity (AnalyzeCodeActivity) + for _, state := range spec.States { + if state.Resource == "AnalyzeCodeActivity" { + if state.Retry == nil { + t.Error("expected retry policy on flaky activity") + } else if state.Retry.MaxAttempts != 3 { + t.Errorf("expected 3 max attempts for flaky activity, got %d", state.Retry.MaxAttempts) + } + if len(state.Catch) == 0 { + t.Error("expected catch clause on flaky activity") + } + } + } +} + +func TestBuildCronSpec(t *testing.T) { + kb, err := LoadKnowledgeBaseFromDefaultPath() + if err != nil { + t.Fatalf("failed to load knowledge base: %v", err) + } + + router := &LLMRouter{ + knowledgeBase: kb, + } + + intent := &Intent{ + Activities: []string{"CloneRepoActivity", "SecurityScanActivity"}, + Parameters: map[string]interface{}{"repo": "https://github.com/test/repo"}, + IsCron: true, + CronSchedule: "0 2 * * *", + CronTimezone: "UTC", + WorkflowName: "daily-scan", + } + + input := LLMRouterInput{ + Message: "Run security scan daily at 2 AM", + } + + cronSpec, err := router.buildCronSpec(intent, input) + if err != nil { + t.Fatalf("buildCronSpec failed: %v", err) + } + + if cronSpec.Type != "CronWorkflow" { + t.Errorf("expected type 'CronWorkflow', got %s", cronSpec.Type) + } + if cronSpec.Schedule != "0 2 * * *" { + t.Errorf("expected schedule '0 2 * * *', got %s", cronSpec.Schedule) + } + if cronSpec.Timezone != "UTC" { + t.Errorf("expected timezone 'UTC', got %s", cronSpec.Timezone) + } + if !cronSpec.EnableHistory { + t.Error("expected EnableHistory=true") + } +} + +func TestBuildParameters(t *testing.T) { + kb, err := LoadKnowledgeBaseFromDefaultPath() + if err != nil { + t.Fatalf("failed to load knowledge base: %v", err) + } + + router := &LLMRouter{ + knowledgeBase: kb, + } + + // Test first activity (CloneRepoActivity) - should use input references + cloneAct := kb.GetActivity("CloneRepoActivity") + intent := &Intent{ + Activities: []string{"CloneRepoActivity", "AnalyzeCodeActivity"}, + Parameters: map[string]interface{}{"repo": "https://github.com/test/repo"}, + } + + params := router.buildParameters(cloneAct, intent, 0) + if params["repo"] != "https://github.com/test/repo" { + t.Errorf("expected repo from parameters, got %v", params["repo"]) + } + + // Test second activity (AnalyzeCodeActivity) - should reference previous output + analyzeAct := kb.GetActivity("AnalyzeCodeActivity") + params = router.buildParameters(analyzeAct, intent, 1) + if params["path"] != "${CloneRepoActivity.output.path}" { + t.Errorf("expected JSONPath reference to CloneRepoActivity.output.path, got %v", params["path"]) + } +} + +func TestBuildRetryPolicy(t *testing.T) { + kb, err := LoadKnowledgeBaseFromDefaultPath() + if err != nil { + t.Fatalf("failed to load knowledge base: %v", err) + } + + router := &LLMRouter{ + knowledgeBase: kb, + } + + // Flaky activity with retry error handling + analyzeAct := kb.GetActivity("AnalyzeCodeActivity") + intent := &Intent{ErrorHandling: "retry"} + policy := router.buildRetryPolicy(analyzeAct, intent) + + if policy.MaxAttempts != 3 { + t.Errorf("expected 3 max attempts for flaky activity, got %d", policy.MaxAttempts) + } + if policy.BackoffRate != 2.0 { + t.Errorf("expected backoff rate 2.0, got %f", policy.BackoffRate) + } + + // Fail-fast error handling + intent = &Intent{ErrorHandling: "fail-fast"} + policy = router.buildRetryPolicy(analyzeAct, intent) + + if policy.MaxAttempts != 1 { + t.Errorf("expected 1 max attempt for fail-fast, got %d", policy.MaxAttempts) + } +} + +func TestIntentJSONMarshal(t *testing.T) { + intent := &Intent{ + Activities: []string{"CloneRepoActivity"}, + Parameters: map[string]interface{}{"repo": "https://test"}, + IsCron: true, + CronSchedule: "0 * * * *", + CronTimezone: "UTC", + WorkflowName: "test", + ErrorHandling: "retry", + } + + data, err := json.Marshal(intent) + if err != nil { + t.Fatalf("marshal failed: %v", err) + } + + var decoded Intent + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal failed: %v", err) + } + + if decoded.CronSchedule != intent.CronSchedule { + t.Errorf("expected schedule %s, got %s", intent.CronSchedule, decoded.CronSchedule) + } +} diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml index 4432036..9afdb30 100644 --- a/k8s/configmap.yaml +++ b/k8s/configmap.yaml @@ -6,4 +6,5 @@ metadata: data: TEMPORAL_NAMESPACE: "poimen-harness" TEMPORAL_HOSTPORT: "temporal-frontend.temporal:7233" - # ANTHROPIC_API_KEY is handled via Secret + LOCAL_LLM_BASE_URL: "http://api-gateway.api:8080" + POIMEN_MEMORY_URL: "http://poimen-memory.poimen.svc.cluster.local:8080" diff --git a/k8s/git-commit.yaml b/k8s/git-commit.yaml index 758ff94..7b1f5ab 100644 --- a/k8s/git-commit.yaml +++ b/k8s/git-commit.yaml @@ -9,6 +9,6 @@ metadata: app.kubernetes.io/name: poimen app.kubernetes.io/component: orchestrator data: - GIT_COMMIT: "38dd3f8d" # Updated automatically by CI/CD + GIT_COMMIT: "303e78f7" # Updated automatically by CI/CD GIT_BRANCH: "main" - DEPLOYMENT_DATE: "2026-08-31" + DEPLOYMENT_DATE: "2026-09-02" diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml index 86bcc25..9c8ef72 100644 --- a/k8s/kustomization.yaml +++ b/k8s/kustomization.yaml @@ -4,23 +4,15 @@ kind: Kustomization namespace: poimen resources: - - orchestrator-job.yaml - worker-deployment.yaml - - git-commit.yaml + - configmap.yaml commonLabels: app.kubernetes.io/name: poimen - app.kubernetes.io/component: orchestrator + app.kubernetes.io/component: worker secretGenerator: - name: poimen-secrets envs: - secrets.env behavior: create - -configMapGenerator: - - name: poimen-config - literals: - - TEMPORAL_NAMESPACE=poimen-harness - - TEMPORAL_HOSTPORT=temporal-frontend.temporal:7233 - behavior: create diff --git a/k8s/orchestrator-job.yaml b/k8s/orchestrator-job.yaml deleted file mode 100644 index 2363be6..0000000 --- a/k8s/orchestrator-job.yaml +++ /dev/null @@ -1,63 +0,0 @@ -apiVersion: batch/v1 -kind: Job -metadata: - name: poimen-orchestrator - namespace: poimen -spec: - backoffLimit: 3 - template: - metadata: - labels: - app: poimen-orchestrator - spec: - restartPolicy: Never - containers: - - name: orchestrator - image: golang:latest - imagePullPolicy: Always # βœ… Force latest image pull - workingDir: /app - command: ["/bin/sh", "-c"] - args: - - | - set -e - echo "[$(date)] Starting poimen orchestrator job..." - apt-get update && apt-get install -y --no-install-recommends git - echo "[$(date)] Cloning latest code from git..." - git clone https://forgejo.riotpiao.com/rock/poimen-workflows.git /app - cd /app - echo "[$(date)] Latest commit: $(git rev-parse HEAD)" - echo "[$(date)] Downloading dependencies..." - go mod download - echo "[$(date)] Starting orchestrator with T0-T4 complete implementation..." - go run ./cmd/starter \ - --repo https://forgejo.riotpiao.com/rock/poimen \ - --remote file:///tmp/poimen-output \ - --milestone T0 \ - --planner-model ornith \ - --judge-model ornith \ - --implementer-model claude-sonnet-5 - env: - - name: TEMPORAL_NAMESPACE - valueFrom: - configMapKeyRef: - name: poimen-config - key: TEMPORAL_NAMESPACE - - name: TEMPORAL_HOSTPORT - valueFrom: - configMapKeyRef: - name: poimen-config - key: TEMPORAL_HOSTPORT - - name: ANTHROPIC_API_KEY - valueFrom: - secretKeyRef: - name: poimen-secrets - key: ANTHROPIC_API_KEY - - name: LOCAL_LLM_BASE_URL - value: "http://api-gateway.api:8080" - resources: - requests: - memory: "512Mi" - cpu: "500m" - limits: - memory: "2Gi" - cpu: "2000m" diff --git a/k8s/worker-deployment.yaml b/k8s/worker-deployment.yaml index 7b043aa..74c75ab 100644 --- a/k8s/worker-deployment.yaml +++ b/k8s/worker-deployment.yaml @@ -13,8 +13,8 @@ spec: labels: app: poimen-worker annotations: - git-commit: "38dd3f8d" # βœ… Updated on each push, triggers rolling restart - deployment-date: "2026-08-31" + git-commit: "303e78f7" # βœ… Updated on each push, triggers rolling restart + deployment-date: "2026-09-02" spec: containers: - name: worker @@ -52,7 +52,15 @@ spec: name: poimen-secrets key: ANTHROPIC_API_KEY - name: LOCAL_LLM_BASE_URL - value: "http://api-gateway.api:8080" + valueFrom: + configMapKeyRef: + name: poimen-config + key: LOCAL_LLM_BASE_URL + - name: POIMEN_MEMORY_URL + valueFrom: + configMapKeyRef: + name: poimen-config + key: POIMEN_MEMORY_URL resources: requests: memory: "512Mi" diff --git a/starter b/starter index 57ee5cb..0f125fb 100755 Binary files a/starter and b/starter differ diff --git a/statemachine/routing_workflow.go b/statemachine/routing_workflow.go new file mode 100644 index 0000000..564ba8f --- /dev/null +++ b/statemachine/routing_workflow.go @@ -0,0 +1,228 @@ +package statemachine + +import ( + "fmt" + "time" + + "github.com/rockliang/poimen/workflows/internal/routing" + "go.temporal.io/sdk/log" + "go.temporal.io/sdk/temporal" + "go.temporal.io/sdk/workflow" +) + +// RoutingWorkflowInput is input for the routing workflow +type RoutingWorkflowInput struct { + Spec *routing.WorkflowSpec `json:"spec"` +} + +// RoutingWorkflowOutput is output from the routing workflow +type RoutingWorkflowOutput struct { + Status string `json:"status"` // "COMPLETED", "FAILED" + FinalOutput interface{} `json:"finalOutput,omitempty"` + StepResults map[string]interface{} `json:"stepResults"` + Error string `json:"error,omitempty"` +} + +// RoutingWorkflow executes any WorkflowSpec generated by llm-router +func RoutingWorkflow(ctx workflow.Context, input RoutingWorkflowInput) (RoutingWorkflowOutput, error) { + logger := workflow.GetLogger(ctx) + + output := RoutingWorkflowOutput{ + Status: "FAILED", + StepResults: make(map[string]interface{}), + } + + if input.Spec == nil || len(input.Spec.States) == 0 { + output.Error = "empty workflow spec" + return output, nil + } + + logger.Info("RoutingWorkflow started", "name", input.Spec.Name, "stateCount", len(input.Spec.States)) + + // Build execution context + execCtx := &routing.ExecutionContext{ + Input: input.Spec.Input, + StepResults: make(map[string]interface{}), + } + + // Build state index for fast lookup + stateIndex := make(map[string]*routing.State) + for i := range input.Spec.States { + stateIndex[input.Spec.States[i].Name] = &input.Spec.States[i] + } + + // Find first state (first in array) + currentStateName := input.Spec.States[0].Name + + // State machine loop + for { + state, ok := stateIndex[currentStateName] + if !ok { + output.Error = fmt.Sprintf("state not found: %s", currentStateName) + return output, nil + } + + logger.Info("executing state", "state", currentStateName, "type", state.Type) + + switch state.Type { + case routing.StateTypeTask: + result, nextState, err := executeTaskState(ctx, state, execCtx, logger) + if err != nil { + // Check for catch clause + if nextState != "" { + currentStateName = nextState + continue + } + output.Error = fmt.Sprintf("state %s failed: %v", currentStateName, err) + return output, nil + } + // Wrap result in output key for JSONPath compatibility (e.g., ${Clone.output.path}) + wrappedResult := map[string]interface{}{"output": result} + execCtx.StepResults[state.Name] = wrappedResult + output.StepResults[state.Name] = result // Keep original for output + + if state.End { + output.Status = "COMPLETED" + output.FinalOutput = result + logger.Info("RoutingWorkflow completed", "name", input.Spec.Name) + return output, nil + } + currentStateName = state.Next + + case routing.StateTypePass: + execCtx.StepResults[state.Name] = state.Result + output.StepResults[state.Name] = state.Result + + if state.End { + output.Status = "COMPLETED" + output.FinalOutput = state.Result + return output, nil + } + currentStateName = state.Next + + case routing.StateTypeFail: + output.Error = fmt.Sprintf("%s: %s", state.Error, state.Cause) + logger.Error("RoutingWorkflow failed at Fail state", "state", currentStateName, "error", state.Error) + return output, nil + + default: + output.Error = fmt.Sprintf("unknown state type: %s", state.Type) + return output, nil + } + + // Safety check + if currentStateName == "" { + output.Error = "no next state and not end" + return output, nil + } + } +} + +// executeTaskState executes a Task state with retry policy +func executeTaskState(ctx workflow.Context, state *routing.State, execCtx *routing.ExecutionContext, logger log.Logger) (interface{}, string, error) { + // Parse timeout + timeout := 5 * time.Minute + if state.Timeout != "" { + if parsed, err := time.ParseDuration(state.Timeout); err == nil { + timeout = parsed + } + } + + // Build activity options + activityOpts := workflow.ActivityOptions{ + StartToCloseTimeout: timeout, + ScheduleToCloseTimeout: timeout + 5*time.Minute, + } + + // Add retry policy if specified + if state.Retry != nil { + initialInterval := time.Second + if state.Retry.InitialInterval != "" { + if parsed, err := time.ParseDuration(state.Retry.InitialInterval); err == nil { + initialInterval = parsed + } + } + maxInterval := 30 * time.Second + if state.Retry.MaxInterval != "" { + if parsed, err := time.ParseDuration(state.Retry.MaxInterval); err == nil { + maxInterval = parsed + } + } + + activityOpts.RetryPolicy = &temporal.RetryPolicy{ + InitialInterval: initialInterval, + BackoffCoefficient: state.Retry.BackoffRate, + MaximumInterval: maxInterval, + MaximumAttempts: state.Retry.MaxAttempts, + } + } + + actCtx := workflow.WithActivityOptions(ctx, activityOpts) + + // Resolve parameters using JSONPath + resolver := routing.NewJSONPathResolver(execCtx.Input, execCtx.StepResults) + resolvedParams, err := resolver.ResolvePaths(state.Parameters) + if err != nil { + return nil, "", fmt.Errorf("failed to resolve parameters: %w", err) + } + + logger.Info("executing activity", "activity", state.Resource, "params", resolvedParams) + + // Execute activity + var result interface{} + err = workflow.ExecuteActivity(actCtx, state.Resource, resolvedParams).Get(ctx, &result) + + if err != nil { + logger.Error("activity failed", "activity", state.Resource, "error", err) + + // Check for catch clauses + for _, catch := range state.Catch { + if matchesError(err, catch.ErrorEquals) { + logger.Info("error caught", "handler", catch.Next) + return nil, catch.Next, err + } + } + return nil, "", err + } + + logger.Info("activity completed", "activity", state.Resource) + return result, "", nil +} + +// matchesError checks if error matches any of the error types +func matchesError(err error, errorEquals []string) bool { + errStr := err.Error() + for _, errType := range errorEquals { + switch errType { + case "ActivityError": + return true // Match all activity errors + case "TimeoutError": + if temporal.IsTimeoutError(err) { + return true + } + case "ApplicationError": + if temporal.IsApplicationError(err) { + return true + } + default: + // Match by error string contains + if contains(errStr, errType) { + return true + } + } + } + return false +} + +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr)) +} + +func containsHelper(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/tasks/HANDOFF_PROMPT.md b/tasks/HANDOFF_PROMPT.md deleted file mode 100644 index 17eaa2c..0000000 --- a/tasks/HANDOFF_PROMPT.md +++ /dev/null @@ -1,137 +0,0 @@ -# Handoff Prompt: Implement T0 Milestone - -Use this prompt with Claude Haiku 4.5 to begin implementation of T0 tasks. - ---- - -## Context - -You're implementing a Temporal-based multi-agent software orchestrator in Go. The system drives development work on arbitrary target repos using three LLM-backed roles: -- **Planner** (reasoning model): reconciles state, dispatches tasks -- **Judge** (reasoning model): reviews correctness, runs integration tests -- **Implementer** (cheaper model): does actual work, learns from failures - -All code lives in `/Users/rockliang/workplace/Poimen/workflows/`. - -## What's Done - -- βœ… Full design doc: `/Users/rockliang/.claude/plans/considered-u-are-a-curried-reef.md` -- βœ… Project scaffold: `PLAN.md`, `tasks/INDEX.md`, `tasks/board.md` -- βœ… Roadmap: T0 (core, 9 tasks) β†’ T1 (hardening) β†’ T2 (scale) β†’ T3 (features) -- βœ… Task breakdown: `tasks/T0.1.md` through `tasks/T0.9.md` (each with verification criteria) - -## Your Job: Implement T0.1 β†’ T0.9 - -Start with T0.1 (repo scaffold). Each task: -1. Read its markdown file in `tasks/T0.x.md` -2. Implement the code sketches provided -3. Write unit tests per verification section -4. Run the verification command -5. When it passes, mark `[ ]` β†’ `[x]` in `tasks/board.md` -6. Move to next task - -## Key Constraints - -1. **No hardcoded values:** All timeouts, retry counts, model IDs come from `OrchestratorConfig.Tuning` or `PromptSpec.Model` (read at runtime). -2. **Activities are independent:** One concern per `action/*.go` file (git, skills, planner, judge, implementer, etc). Testable in isolation. -3. **Testing is verification:** Unit tests via `go.temporal.io/sdk/testsuite` (mocked activities). E2E test (T0.9) against real `temporal.riotpiao.com`. -4. **Go style:** Per `golang-skills` conventions β€” no naked `_ =`, proper error handling, idiomatic names. -5. **Concurrency safety:** Shared FS with git worktrees + advisory lock. Test concurrent access. - -## Implementation Path - -``` -1. T0.1: Scaffold directories, go.mod, empty stubs - β†’ verify: go build ./... succeeds - -2. T0.2: Shared types (ModelSpec, PromptSpec, OrchestratorConfig, etc.) - β†’ verify: Unit test asserts all defaults - -3. T0.3: Git & locking (CloneRepoActivity, worktrees, squash-merge) - β†’ verify: Test against local scratch repo - -4. T0.4: Pi & error classification (PrepareSkillsActivity, classifyPiErr) - β†’ verify: Unit tests for 4xx/5xx/504 buckets - -5. T0.5: LLM agents & prompts (Planner/Judge/Implementer, llm/client.go) - β†’ verify: Unit test renders PromptSpec with system prompt - -6. T0.6: TaskUnit workflow (retry loops, timeout escalation, lessons) - β†’ verify: Testsuite: pass-first-try, fail-then-pass, timeout-escalation - -7. T0.7: Orchestrator workflow (config, signals, fan-out/fan-in, continue-as-new, 504 learning) - β†’ verify: Testsuite: fan-out/fan-in, squash-merge, signal mutations - -8. T0.8: Worker & starter CLIs (cmd/worker, cmd/starter, internal/config) - β†’ verify: go build succeeds, go run ./cmd/worker connects to temporal.riotpiao.com - -9. T0.9: End-to-end (real cluster + disposable forgejo repo, all 7 checks) - β†’ verify: Full cycle with live signals, 5xx retry/exhaust, 504 learning, continue-as-new bounded -``` - -## Tools Available - -- `pi` command: Clone/fetch skills from homelab API - - Usage: `pi clone-or-fetch ` - - Errors: 4xx (non-retryable), 5xx (retryable), 504 (stream timeout β€” learn and double timeout) - -- Temporal Web UI: `http://temporal.riotpiao.com:8080` (monitor workflows) -- Forgejo instance: For disposable scratch repos during testing - -## Testing Locally - -Each task has a "Verification" section with a test command. Run it after implementing: - -```bash -cd /Users/rockliang/workplace/Poimen/workflows -go test -v ./tests -run Test -``` - -For T0.9 (e2e), you'll need: -- Real Temporal cluster connection -- Anthropic API key (`ANTHROPIC_API_KEY` env) -- Forgejo repo access or local git repo - -## Commit Message Style - -``` -T0.x: Brief description - -Detailed explanation of what was implemented. - -Verification: -``` - -Example: -``` -T0.2: Implement shared types with defaults - -Added ModelSpec, PromptSpec, OrchestratorConfig, ActivityTuning, PiRetryPolicy. -All defaults documented: 5m ScheduleToCloseTimeout, 2s InitialInterval, 2.0 BackoffCoefficient, etc. - -Verification: go test ./tests -run TestTypesDefaults passes -``` - -## When Stuck - -1. Re-read the task markdown in `tasks/T0.x.md` β€” implementation sketches are concrete -2. Check `PLAN.md` Β§Design sections for detailed logic (Β§Timeout Extension, Β§Pi Command Retry Policy, etc.) -3. Look at the test case in the task markdown β€” it shows expected behavior -4. If a task depends on prior work, make sure the prior task is complete first - -## After T0 Completes - -When all T0.1–T0.9 pass: -```bash -cd /Users/rockliang/workplace/Poimen/workflows -git checkout main -git merge --squash task/T0.1 task/T0.2 ... task/T0.9 -git commit -m "T0: Multi-agent orchestrator initial implementation" -git push origin main -``` - -Then start T1 (or hand off to another assistant). - ---- - -**Ready?** Start with `tasks/T0.1.md` and work through in order. diff --git a/tasks/INDEX.md b/tasks/INDEX.md deleted file mode 100644 index 68f71b2..0000000 --- a/tasks/INDEX.md +++ /dev/null @@ -1,125 +0,0 @@ -# Poimen Workflows β€” Development Guidelines - -This project orchestrates multi-agent software development tasks using Temporal + Go. The repo is organized as: -- `statemachine/` β€” Temporal workflow definitions (deterministic state machines) -- `action/` β€” Temporal activity definitions (units of work / LLM calls) -- `cmd/` β€” CLI entry points (worker registration, workflow starter) -- `prompts/` β€” LLM prompt templates (Go-embedded, live-updatable) -- `internal/` β€” Shared config/locking utilities -- `tests/` β€” Unit tests via `go.temporal.io/sdk/testsuite` - -## Code Standards - -### Go Style - -Follow `golang-skills` conventions from `~/.claude/skills/golang-skills/`: -- Error handling mandatory; no naked `_ =` discards. -- Idiomatic naming: `ctx` for context, `err` for error returns. -- Interfaces kept narrow; one struct per concern. -- No over-generalization for hypothetical use. -- Documentation via comments only where the WHY is non-obvious. - -### Testing - -- Every `statemachine/*` change ships with a `testsuite`-based test in `tests/`. -- Unit tests use `go.temporal.io/sdk/testsuite.WorkflowTestEnvironment` with mocked activities. -- Activities are tested in isolation before integrating into workflows. -- No literal timeout/retry values in `action/*` code β€” all come from `OrchestratorConfig.Tuning` at runtime. - -### Activity Design - -Activities stay side-effect-isolated (one concern per file) so they remain independently reusable across projects. -- `action/git.go` β€” all git operations -- `action/skills.go` β€” skill prep / pi command -- `action/planner.go` β€” Planner LLM call -- `action/judge.go` β€” Judge LLM call -- `action/implementer.go` β€” Implementer LLM + tool-call loop -- `action/lessons.go` β€” Lessons store read/write - -No composite activities like `PrepareAndImplement`; that's a workflow's job, not an activity's. - -### Config as Data - -**No hardcoded numbers.** Every timeout, retry count, backoff coefficient, stream timeout β€” all come from `OrchestratorConfig.Tuning` (or role-specific `PromptSpec.Model`), read at execution time. This lets: -- `update-tuning` signal to dynamically adjust timeouts without redeployment -- Planner activity to recommend per-task overrides -- Pi 504 handler to learn and persist `StreamTimeout` across `ContinueAsNew` cycles - -**No LLM model hardcoded.** Every model ID comes from `ModelSpec.ModelID`, passed as data. - -### Concurrency & Locking - -Target repo sits on shared filesystem. Git concurrency is safe by construction: -- Task units use isolated worktrees (`git worktree add -b task/T0.x ...`) -- Commits within a worktree don't need a lock; git serializes object writes -- Only `CloneRepoActivity`, `GitPushActivity`, `GitSquashMergeActivity` need the `orchestrator.lock`, since they mutate the main working tree / refs - -Monitor for deadlocks: if two workflows try to push simultaneously, the lock will serialize them. Test with concurrent tasks enabled. - -## Task Board Format - -Each task row in the board specifies: -- **ID**: `T0.1`, `T0.2`, etc. -- **Description**: One-line scope -- **Status**: `[ ]` (todo), `[x]` (done) -- **Branch**: `task/T0.x` (created when task starts, merged to main on submilestone complete) -- **Verification**: Specific criterion that marks it done (e.g. "Unit test passes", "E2E run completes") - -A task is NOT marked done until its verification step passes. This mirrors the Judge role's own job: no hallucinated completion. - -## Submilestones & Merges - -When all tasks in `T0` (`T0.1` through `T0.9`) pass their verification: -1. Orchestrator calls `GitSquashMergeActivity` to merge all `task/T0.*` branches into `main` as a single squashed commit -2. All `task/T0.*` branches and worktrees are cleaned up -3. Main branch is the canonical history; the 9 subtask commits are compacted into one - -This demonstrates the very mechanism the system orchestrates: a working software dev pipeline with multiple agents collaborating on a shared codebase, guarded by state checks (Judge), and landing changes via deterministic git workflow. - -## Historical Lessons - -Lessons live at `tasks/.orchestrator/lessons/.jsonl` β€” per-task file of failed attempts. When a Judge calls a task failure, the `UpdateLessonsActivity` appends `{Attempt, Critique, FailedApproachSummary, Timestamp}`. On retry, `ReadLessonsActivity` injects the last N entries into the Implementer's prompt as "known errors β€” do not repeat this time". - -Lessons flush to git only as part of the Planner's board commit, not on every retry β€” keeps history clean. - -## Skills & Preparation - -Required skill sources are listed in `OrchestratorConfig.Skills` as a list of references (e.g. `["~/.claude/skills/golang-skills", "custom-skill-repo"]`). `PrepareSkillsActivity` runs once per config change, cloning/fetching them onto the shared FS via the `pi` command. All retry/backoff/timeout is delegated to Temporal's retry machinery, with special handling for 504 (stream timeout learning). - -## Environment & Secrets - -All external system access is via environment variables, loaded at worker startup: -- `TEMPORAL_NAMESPACE`, `TEMPORAL_TLS_CERT`, `TEMPORAL_TLS_KEY` β€” Temporal cluster connection -- `ANTHROPIC_API_KEY` β€” LLM API key -- Any target-repo-specific credentials (e.g. git SSH key) are assumed already available on the shared FS (e.g. via Kubernetes secret mount) - -Use homelab's `vsource .env` pattern to load from a `.env` file during local development. - -## Observability - -- Temporal Web UI (`temporal.riotpiao.com:8080` or similar) shows workflow execution, signal delivery, activity retries -- Workflow `current-config` query returns live `OrchestratorConfig` (useful for debugging which tuning values are in effect) -- Activity heartbeats (`activity.RecordHeartbeat`) are sent after each tool-call iteration, visible in Temporal's activity details -- Lessons file grows as retries happen; inspect it on failure to understand what the Implementer is learning - -## Deployment & Homelab Integration - -This repo is application-layer code against the homelab's Temporal cluster. The orchestrator runs as a Kubernetes pod with: -- Persistent volume (PVC) for the shared FS where target repos are cloned -- Network access to Temporal + Anthropic APIs -- Git SSH key mounted for cloning target repos - -The Kubernetes manifests + Helm charts live in the homelab repo under `k8s/` and follow homelab's GitOps workflow (commit β†’ ArgoCD sync). This repo's CI/CD (GitHub Actions or Forgejo Actions) builds and pushes the Docker image; the homelab repo triggers a new pod deploy on image push. - -## Questions & Debugging - -If a task is marked done but you suspect it's wrong: -1. Re-run its verification step manually -2. Check the unit test against the latest code -3. For e2e tasks, review Temporal Web UI logs + board file + lessons file on the target repo -4. Update the board and task description if the criterion was misunderstood - -If code doesn't compile or tests fail: -1. Check Go version and Temporal SDK version match -2. Run `go mod tidy` and `go mod vendor` if dependencies drift -3. Look for hardcoded values or model IDs that should be config instead diff --git a/tasks/ROADMAP.md b/tasks/ROADMAP.md deleted file mode 100644 index e33a512..0000000 --- a/tasks/ROADMAP.md +++ /dev/null @@ -1,102 +0,0 @@ -# Project Roadmap - -Multi-agent dev orchestrator for Temporal, building toward a fully autonomous software development system. - -## Milestones - -### T0: Core System (IN PROGRESS) -9 tasks, foundational: Planner/Judge/Implementer orchestration, git workflow, LLM integration, e2e validation. - -**Status:** 9 subtasks defined, testable criteria in place. -**Timeline:** ~2-4 weeks to implementation. -**Done:** All T0.1–T0.9 pass verification β†’ squash-merge to main. - -**Board:** `tasks/board.md` - ---- - -### T1: Production Hardening (PLANNED) -8 tasks: Error recovery, observability, metrics, audit logging, health checks. - -**Focus:** Reliability for long-running orchestrators in homelab. -**Key wins:** -- Resume from crash without data loss -- Structured logging + Grafana metrics -- Auto-tuning based on historical failures -- Audit trail for compliance - -**Board:** `tasks/board-T1.md` - ---- - -### T2: Scale & Performance (PLANNED) -8 tasks: Caching, parallelism, batching, distributed locking. - -**Focus:** Handle 100s of concurrent tasks, reduce API call overhead. -**Key wins:** -- Result caching deduplicates LLM calls -- Parallel task dispatch (9x wall-clock speedup) -- Git operation batching (fewer network round-trips) -- Distributed lock for multi-pod safety - -**Board:** `tasks/board-T2.md` - ---- - -### T3: Feature Expansion (PLANNED) -8 tasks: Plugins, templates, dependencies, human gates, custom judges, nested workflows. - -**Focus:** Extensibility + domain specialization. -**Key wins:** -- Load custom skill plugins -- Save/load orchestrator config as templates -- Task dependency ordering -- Swap Judge for domain-specific validator (security auditor, code reviewer, etc.) -- Nest orchestrators (multi-level hierarchy) -- Import tasks from GitHub/Linear/JIRA - -**Board:** `tasks/board-T3.md` - ---- - -## Implementation Order - -1. **T0 (2-4 weeks):** Core system working end-to-end. -2. **T1 (2 weeks):** Production-harden the core. -3. **T2 (3 weeks):** Scale & optimize. -4. **T3 (4 weeks):** Advanced features. - -**Total:** ~3 months to full feature parity. - ---- - -## Success Metrics - -- **T0:** System runs unsupervised on 9-task milestone, all subtasks auto-complete, squash-merge to main succeeds. -- **T1:** 1000+ tasks completed, zero data loss across pod restarts, metrics queryable. -- **T2:** 1000 concurrent tasks complete 10x faster than T0, < 5 API calls/task (vs current ~20). -- **T3:** Custom Judge plugin loads and validates tasks, workflow templates save/restore state. - ---- - -## Risk Mitigation - -| Risk | Mitigation | -|------|-----------| -| Temporal cluster unavailability | Implement activity heartbeat recovery + resumption logic (T1.1) | -| Git conflicts on shared FS | Test multi-pod concurrent access (T1.6) β†’ upgrade to distributed lock if needed (T2.8) | -| LLM API latency | Implement result caching + batching (T2.1, T2.6) | -| Board state corruption | Detect + auto-heal (T1.4) | -| Audit trail tampering | Sign audit log with workflow key (T3.6) | - ---- - -## Notes - -- Each milestone is independent: T1 can start once T0 core is complete, doesn't need full T0 cleanup. -- T0 tasks are foundational; changes will ripple into T1–T3 test suites, but core API should remain stable. -- Future milestones (T4+) could focus on: web UI, real-time dashboard, automated deployment of orchestrator itself (meta!), multi-cluster orchestration. - ---- - -**Next:** Start T0.1 (repo scaffold). diff --git a/tasks/T0.1.md b/tasks/T0.1.md deleted file mode 100644 index 5210633..0000000 --- a/tasks/T0.1.md +++ /dev/null @@ -1,42 +0,0 @@ -# T0.1: Repo Scaffold - -## Scope -Create directory structure, `go.mod`, empty stubs for all packages. - -## Implementation Checklist -- [ ] `go.mod`: module `github.com/rockliang/poimen/workflows`, Go 1.21+ -- [ ] `statemachine/types.go`: empty package stub (will fill in T0.2) -- [ ] `statemachine/signals.go`: empty package stub -- [ ] `statemachine/orchestrator.go`: empty package stub, func placeholder -- [ ] `statemachine/taskunit.go`: empty package stub, func placeholder -- [ ] `action/planner.go`: empty package stub -- [ ] `action/implementer.go`: empty package stub -- [ ] `action/judge.go`: empty package stub -- [ ] `action/git.go`: empty package stub -- [ ] `action/skills.go`: empty package stub -- [ ] `action/integration_test.go`: empty package stub -- [ ] `action/lessons.go`: empty package stub -- [ ] `action/llm/client.go`: empty package stub -- [ ] `prompts/registry.go`: empty package stub -- [ ] `prompts/planner/default.tmpl`: empty text file -- [ ] `prompts/judge/default.tmpl`: empty text file -- [ ] `prompts/implementer/default.tmpl`: empty text file -- [ ] `internal/config/config.go`: empty package stub -- [ ] `internal/lock/flock.go`: empty package stub -- [ ] `cmd/worker/main.go`: `func main()` stub -- [ ] `cmd/starter/main.go`: `func main()` stub -- [ ] `tests/taskunit_workflow_test.go`: empty test file -- [ ] `tests/orchestrator_workflow_test.go`: empty test file - -## Verification -```bash -cd /Users/rockliang/workplace/Poimen/workflows -go build ./... -# Command should succeed with no errors -# All directories should exist as listed above -``` - -## Done Criteria -- `go build ./...` succeeds with exit code 0 -- `ls -R` shows all directories match PLAN.md Β§Directory Structure -- No compilation errors or warnings diff --git a/tasks/T0.2.md b/tasks/T0.2.md deleted file mode 100644 index 4676ebd..0000000 --- a/tasks/T0.2.md +++ /dev/null @@ -1,108 +0,0 @@ -# T0.2: Shared Types - -## Scope -Implement `statemachine/types.go` with all config/input/output structs and document defaults. - -## Implementation -File: `statemachine/types.go` - -```go -type ModelSpec struct { - ModelID string // e.g. "claude-opus-5", "claude-sonnet-5" - Thinking string // "adaptive" or "" - Effort string // "low", "medium", "high", "xhigh", "max" -} - -type PromptSpec struct { - TemplateRef string // e.g. "planner/default.tmpl" - RawTemplate string // overrides TemplateRef if non-empty - Variables map[string]any - Model ModelSpec - LessonsRef string // key into lessons store -} - -type PiRetryPolicy struct { - ScheduleToCloseTimeout time.Duration // default: 5m - InitialInterval time.Duration // default: 2s - MaximumInterval time.Duration // default: 30s - BackoffCoefficient float64 // default: 2.0 - StreamTimeout time.Duration // default: 30s - StreamTimeoutMax time.Duration // default: 2m -} - -type ActivityTuning struct { - ImplementerBaseTimeout time.Duration // default: 10m - ImplementerMaxRetries int // default: 3 - JudgeTimeout time.Duration // default: 5m - PiRetry PiRetryPolicy -} - -type OrchestratorConfig struct { - SystemPrompt string // shared prompt prefix - Skills []SkillRef // required skill sources - RolePrompts map[string]PromptSpec // per-role: "planner", "judge", "implementer" - Tuning ActivityTuning -} - -type OrchestratorInput struct { - TargetRepoPath string - RemoteURL string - Milestone string // e.g. "T0" - Config OrchestratorConfig - DryRun bool - CycleCount int - MaxCyclesBeforeCAN int // default: 100 -} - -type OrchestratorOutput struct { - MilestoneComplete bool - Done bool - LastError string -} - -type TaskUnitInput struct { - TaskID string - TargetRepoPath string - JudgeSpec PromptSpec - ImplementerSpec PromptSpec - BaseTimeout time.Duration - MaxJudgeRetries int -} - -type TaskUnitOutput struct { - TaskID string - Verdict string // "pass" or "fail" - Critique string - Branch string -} - -type SkillRef struct { - Name string // skill identifier - URL string // source to clone -} -``` - -## Verification -```bash -cd /Users/rockliang/workplace/Poimen/workflows -go build ./statemachine/ - -# Run unit test: -go test -v ./tests -run TestTypesDefaults -``` - -Test file: `tests/types_test.go` -```go -func TestTypesDefaults(t *testing.T) { - // Verify all defaults are correctly set - pr := PiRetryPolicy{} - assert.Equal(t, 5*time.Minute, pr.ScheduleToCloseTimeout) - // ... more assertions -} -``` - -## Done Criteria -- `go build ./statemachine/` succeeds -- `go test ./tests -run TestTypesDefaults` passes -- All struct fields documented with default values -- No compilation errors diff --git a/tasks/T0.3.md b/tasks/T0.3.md deleted file mode 100644 index bd443dc..0000000 --- a/tasks/T0.3.md +++ /dev/null @@ -1,93 +0,0 @@ -# T0.3: Git & Locking - -## Scope -Implement `action/git.go` + `internal/lock/flock.go` for repo cloning, worktree management, and squash-merge. - -## Implementation - -### File: `internal/lock/flock.go` -```go -package lock - -// Acquire advisory file lock (blocking) -func Acquire(path string) error - -// Release advisory file lock -func Release(path string) error -``` - -### File: `action/git.go` -```go -type CloneRepoInput struct { - RemoteURL string - TargetRepoPath string -} - -func CloneRepoActivity(ctx context.Context, in CloneRepoInput) error - // If $TargetRepoPath/.git exists: git -C $TargetRepoPath fetch origin - // Else: git clone $RemoteURL $TargetRepoPath - -type GitWorktreeAddInput struct { - RepoPath string - TaskID string -} - -func GitWorktreeAddActivity(ctx context.Context, in GitWorktreeAddInput) (string, error) - // Guarded by orchestrator.lock - // git worktree add -b task/ ../worktrees/ origin/main - // Return worktree path - -type GitCommitInput struct { - WorktreePath string - Message string -} - -func GitCommitActivity(ctx context.Context, in GitCommitInput) error - // No lock needed; safe within isolated worktree - // git -C $WorktreePath add -A - // git -C $WorktreePath commit -m "$Message" - -type GitPushInput struct { - RepoPath string -} - -func GitPushActivity(ctx context.Context, in GitPushInput) error - // Guarded by orchestrator.lock - // git -C $RepoPath push origin main - -type GitSquashMergeInput struct { - RepoPath string - Branches []string // ["task/T0.1", "task/T0.2", ...] - Message string -} - -func GitSquashMergeActivity(ctx context.Context, in GitSquashMergeInput) error - // Guarded by orchestrator.lock - // fetch origin main - // checkout main && pull --ff-only origin main - // for b in branches: merge --squash $b - // commit -m $Message - // push origin main - // for b in branches: worktree remove + branch -D -``` - -## Verification -```bash -cd /Users/rockliang/workplace/Poimen/workflows -go test -v ./tests -run TestGit - -# Test script: tests/git_test.go -``` - -Test cases: -- Clone into empty path β†’ creates .git -- Clone into existing path β†’ fetches instead of re-cloning -- Worktree add β†’ returns valid path -- Commit in worktree β†’ file changes staged -- Squash-merge β†’ one commit on main, branches cleaned up - -## Done Criteria -- `go test ./tests -run TestGit` passes -- Tested against local scratch git repo (not real remote) -- No lock deadlocks on concurrent calls -- Squash-merge produces exactly one commit diff --git a/tasks/T0.4.md b/tasks/T0.4.md deleted file mode 100644 index 68d97b6..0000000 --- a/tasks/T0.4.md +++ /dev/null @@ -1,72 +0,0 @@ -# T0.4: Pi & Error Classification - -## Scope -Implement `action/skills.go` with `PrepareSkillsActivity` and `classifyPiErr` for skill prep via homelab API. - -## Implementation - -### File: `action/skills.go` -```go -type SkillRef struct { - Name string - URL string -} - -type PrepareSkillsInput struct { - Skills []SkillRef - StreamTimeout time.Duration -} - -func PrepareSkillsActivity(ctx context.Context, in PrepareSkillsInput) error - // For each skill, run: pi clone-or-fetch $skill.URL - // Pass --stream-timeout=$StreamTimeout to pi - // Each skill behind its own lock (not orchestrator.lock) - // On error, return classified error (see below) - -func classifyPiErr(err error) error - // 4xx (400-499): NonRetryableApplicationError "PiClientError" - // 504: ApplicationError "PiStreamTimeout" - // 5xx (500-599, except 504): leave retryable - // Other network errors: leave retryable -``` - -## Error Buckets - -### Bucket 1: 4xx (PiClientError) -- Status code 400-499 -- Non-retryable: bad request, auth error, not found -- Temporal stops retrying immediately -- Activity fails - -### Bucket 2: 5xx except 504 (generic 5xx) -- Status code 500-503, 505-599 -- Retryable: server error, likely transient -- Temporal backs off + retries until `ScheduleToCloseTimeout` (5m) - -### Bucket 3: 504 (PiStreamTimeout) -- Status code 504 -- Means pi's SSE stream-read timed out -- Retryable, BUT: Orchestrator doubles `config.Tuning.PiRetry.StreamTimeout` before retry -- Next attempt uses wider timeout - -## Verification -```bash -cd /Users/rockliang/workplace/Poimen/workflows -go test -v ./tests -run TestPiErrors - -# Test file: tests/pi_test.go -``` - -Test cases: -- Mock 400 response β†’ NonRetryableApplicationError returned -- Mock 403 response β†’ NonRetryableApplicationError returned -- Mock 500 response β†’ retryable error returned -- Mock 503 response β†’ retryable error returned -- Mock 504 response β†’ ApplicationError type "PiStreamTimeout" returned -- Mock network timeout β†’ retryable error returned - -## Done Criteria -- `go test ./tests -run TestPiErrors` passes all 6 test cases -- All error buckets correctly classified -- No panics on nil pointers -- Error messages include HTTP status code diff --git a/tasks/T0.5.md b/tasks/T0.5.md deleted file mode 100644 index 6f3b832..0000000 --- a/tasks/T0.5.md +++ /dev/null @@ -1,133 +0,0 @@ -# T0.5: LLM Agents & Prompts - -## Scope -Implement LLM activities (Planner, Judge, Implementer), LLM client, and prompt template registry. - -## Implementation - -### File: `action/llm/client.go` -```go -type AnthropicClient struct { - apiKey string -} - -func NewClient() *AnthropicClient - // Read ANTHROPIC_API_KEY from env - // Return client - -func (c *AnthropicClient) CreateMessage(ctx context.Context, in MessageInput) (string, error) - // Call Anthropic API messages.create - // Respect model.ModelID, model.Thinking, model.Effort - // Return response text -``` - -### File: `action/planner.go` -```go -type PlanningInput struct { - Config OrchestratorConfig - BoardState string // JSON or markdown of task board - Milestone string -} - -type TaskDispatch struct { - TaskID string - Prompt PromptSpec - BaseTimeout time.Duration // can override default -} - -func PlanningActivity(ctx context.Context, in PlanningInput) ([]TaskDispatch, error) - // Read target repo's tasks/INDEX.md + board from shared FS - // Render prompt: in.Config.SystemPrompt + in.Config.RolePrompts["planner"] template - // Call LLM (Planner model) - // Parse response: which tasks to dispatch next, optional tuning overrides - // Return task dispatch list -``` - -### File: `action/judge.go` -```go -type JudgeInput struct { - Config OrchestratorConfig - Diff string // git diff output - IntegrationTestLogs string // test output -} - -type JudgeOutput struct { - Verdict string // "pass" or "fail" - Critique string // explanation if fail -} - -func JudgeActivity(ctx context.Context, in JudgeInput) (JudgeOutput, error) - // Render prompt: in.Config.SystemPrompt + in.Config.RolePrompts["judge"] - // Call LLM (Judge model, reasoning) - // Parse response: verdict + critique - // Return JudgeOutput -``` - -### File: `action/implementer.go` -```go -type ImplementerInput struct { - Config OrchestratorConfig - TaskID string - WorktreePath string - Lessons string // "known errors β€” do not repeat" section -} - -type ImplementerOutput struct { - Success bool - Changes string // summary of changes made -} - -func ImplementerActivity(ctx context.Context, in ImplementerInput) (ImplementerOutput, error) - // Render prompt: in.Config.SystemPrompt + in.Config.RolePrompts["implementer"] - // Inject in.Lessons into Variables - // Start tool-call agent loop (run git/cargo/pnpm/etc as needed) - // After each tool call, activity.RecordHeartbeat(ctx, progress) - // Return success/changes -``` - -### File: `prompts/registry.go` -```go -// go:embed prompts/*.tmpl - -func Render(templateRef string, variables map[string]any) (string, error) - // Load embedded template via go:embed + text/template - // Render with variables - // Return rendered string -``` - -### Files: `prompts/planner/default.tmpl`, etc. -Empty templates for now; will be filled in by Planner/Judge/Implementer activities. - -``` -You are a Planner agent. Your job: reconcile task state, dispatch work. - -System prompt: {{.SystemPrompt}} - -Current board: -{{.BoardState}} - -Current config: -{{.Config | json}} - -What tasks should we dispatch next? (respond in JSON: {"tasks": [{"id": "T0.1", "timeout_override_ms": null}, ...]}) -``` - -## Verification -```bash -cd /Users/rockliang/workplace/Poimen/workflows -go test -v ./tests -run TestPrompts - -# Test file: tests/prompts_test.go -``` - -Test cases: -- Render template with system prompt + variables β†’ output includes system prompt prefix -- Render with RawTemplate override β†’ uses raw template, not embedded -- Render with Variables substitution β†’ all {{.Var}} replaced -- Mock LLM client responses β†’ activities parse correctly - -## Done Criteria -- `go test ./tests -run TestPrompts` passes -- All templates render without errors -- LLM client reads ANTHROPIC_API_KEY from env (or uses mock in tests) -- Activities parse LLM responses into structured output diff --git a/tasks/T0.6.md b/tasks/T0.6.md deleted file mode 100644 index e51d72a..0000000 --- a/tasks/T0.6.md +++ /dev/null @@ -1,78 +0,0 @@ -# T0.6: TaskUnit Workflow - -## Scope -Implement `statemachine/taskunit.go` with retry loops, timeout escalation, and lessons injection. - -## Implementation - -### File: `statemachine/taskunit.go` -```go -func TaskUnitWorkflow(ctx workflow.Context, in TaskUnitInput) (TaskUnitOutput, error) - // 1. Call GitWorktreeAddActivity(ctx, {RepoPath, TaskID}) β†’ get worktree path - // 2. Lessons file init: try ReadLessonsActivity, may be empty on first run - // 3. Retry loop: - // timeoutAttempt := 1 - // for judgeAttempt := 1; judgeAttempt <= in.MaxJudgeRetries; judgeAttempt++ - // - // SetupActivity options: - // StartToCloseTimeout: in.BaseTimeout * time.Duration(timeoutAttempt) - // HeartbeatTimeout: (in.BaseTimeout * time.Duration(timeoutAttempt)) / 4 - // RetryPolicy: {MaximumAttempts: 1} // NO retries; we manage them in the loop - // - // Call ImplementerActivity(ctx, {Lessons: lessons, ...}) - // If isStartToCloseTimeout(err): - // timeoutAttempt++ - // judgeAttempt-- // don't consume a judge retry on timeout - // continue // next loop iteration has longer timeout - // If err != nil: - // return TaskUnitOutput{Verdict: "fail", Critique: err.Error()}, nil - // - // Call RunIntegrationTestActivity(ctx, {WorktreePath, TestCmd}) - // If integration test fails: - // // Some tasks don't have tests; pass if no test defined - // - // Call JudgeActivity(ctx, {Diff, IntegrationTestResult}) - // If judge.Verdict == "pass": - // Call GitCommitActivity(ctx, {WorktreePath, "T0.x: implementation"}) - // return TaskUnitOutput{Verdict: "pass", Branch: "task/T0.x"} - // Else: - // Call UpdateLessonsActivity(ctx, {Critique}) - // lessons = ReadLessonsActivity() // updated lessons for next attempt - // continue // next judgeAttempt with lessons injected - // End retries - // - // 4. If we exit loop without pass: return fail verdict -``` - -## Retry Logic Detail - -**TimeoutAttempt vs JudgeAttempt:** -- TimeoutAttempt: activity ran out of time, next attempt has longer StartToCloseTimeout -- JudgeAttempt: activity finished but output wrong, lessons injected, duration doesn't change -- They're independent counters so timeout escalation doesn't consume judge retries - -**Lessons injection:** -- Before each ImplementerActivity, render prompt with Lessons appended: "Known errors from prior attempts:\n{lessons}" -- On judge failure, append new lesson to lessons file -- Lessons persist within TaskUnitWorkflow (shared memory) -- Lessons also written to disk (tasks/.orchestrator/lessons/.jsonl) for Planner's review - -## Verification -```bash -cd /Users/rockliang/workplace/Poimen/workflows -go test -v ./tests -run TestTaskUnit - -# Test file: tests/taskunit_workflow_test.go -``` - -Test cases (mocked activities): -1. **Pass on first try:** Implementerβ†’TestPassβ†’JudgePass β†’ return pass -2. **Fail then pass after lesson:** Implementerβ†’TestPassβ†’JudgeFail (append lesson) β†’ Implementer (lessons injected)β†’TestPassβ†’JudgePass β†’ return pass -3. **Retries exhausted:** Implementerβ†’JudgeFail 3 times β†’ return fail -4. **Timeout escalation:** Implementer timeout 1st β†’ BaseTimeout*1 fails, Implementer timeout 2nd β†’ BaseTimeout*2 succeeds β†’ continue - -## Done Criteria -- `go test ./tests -run TestTaskUnit` passes all 4 cases -- Split timeout/judge-fail counters work correctly -- Lessons inject into prompt without error -- No infinite loops on mocked failures diff --git a/tasks/T0.7.md b/tasks/T0.7.md deleted file mode 100644 index ac96705..0000000 --- a/tasks/T0.7.md +++ /dev/null @@ -1,143 +0,0 @@ -# T0.7: Orchestrator Workflow - -## Scope -Implement `statemachine/orchestrator.go` with config state, signals, fan-out/fan-in, continue-as-new, and 504 learning. - -## Implementation - -### File: `statemachine/orchestrator.go` -```go -func OrchestratorWorkflow(ctx workflow.Context, in OrchestratorInput) (OrchestratorOutput, error) - // 1. Mutable config state (not frozen at start): - // config := in.Config - // skillsHaveChanged := true // first cycle, prep skills - // - // 2. Signal handlers (checked each cycle): - // - "update-system-prompt": config.SystemPrompt = signalPayload - // - "update-skills": config.Skills = signalPayload, skillsHaveChanged = true - // - "update-role-prompt": config.RolePrompts[role] = signalPayload - // - "update-tuning": config.Tuning = signalPayload - // - "pause": wait for "resume" signal - // - "abort-task": forward via SignalExternalWorkflow(ctx, "taskunit-"+taskID, "abort", nil) - // - // 3. Query handlers: - // - "status": return current cycle count, pending tasks - // - "current-config": return config - // - // 4. Main loop (continues until submilestone complete): - // for { - // // Check signals (pause, abort, update-*) - // selector := workflow.NewSelector(ctx) - // // register signal channels - // - // // Prep skills if needed - // if skillsHaveChanged { - // call PrepareSkillsActivity(ctx, {config.Skills, config.Tuning.PiRetry.StreamTimeout}) - // wrap in 504-learning loop: - // for { - // err := ExecuteActivity(...) - // if isPiStreamTimeout(err) && config.Tuning.PiRetry.StreamTimeout < config.Tuning.PiRetry.StreamTimeoutMax: - // config.Tuning.PiRetry.StreamTimeout *= 2 - // continue - // break - // } - // skillsHaveChanged = false - // } - // - // // Planning phase 1: decide what to dispatch - // planResult := call PlanningActivity(ctx, {config, boardState, milestone}) - // if submilestoneComplete(planResult): - // // All subtasks done, trigger merge - // call GitSquashMergeActivity(ctx, {repoBranches, "T0: squash merge subtasks"}) - // return OrchestratorOutput{MilestoneComplete: true, Done: true} - // - // // Dispatch: fan out TaskUnitWorkflow for each task - // taskFutures := []workflow.Future{} - // for taskID in planResult.tasksToDispatch: - // spec := selectApplicableSpec(config.RolePrompts, taskID) - // future := ExecuteChildWorkflow(ctx, TaskUnitWorkflow, TaskUnitInput{ - // TaskID: taskID, - // JudgeSpec: config.RolePrompts["judge"], - // ImplementerSpec: config.RolePrompts["implementer"], - // BaseTimeout: config.Tuning.ImplementerBaseTimeout, // or override from planner - // MaxJudgeRetries: config.Tuning.ImplementerMaxRetries, - // }) - // taskFutures = append(taskFutures, future) - // - // // Await all - // results := []TaskUnitOutput{} - // for future in taskFutures: - // var out TaskUnitOutput - // future.Get(ctx, &out) - // results = append(results, out) - // - // // Planning phase 2: update board and commit - // call PlanningActivity(ctx, {config, results, boardState, milestone}) β†’ UpdateBoardOutput - // call GitCommitActivity(ctx, {repoPath, "Update board after cycle"}) - // call GitPushActivity(ctx, {repoPath}) - // - // // Continue-as-new check - // in.CycleCount++ - // if in.CycleCount >= in.MaxCyclesBeforeCAN: - // nextInput := OrchestratorInput{ - // // carry forward all state - // CycleCount: 0, - // Config: config, // includes mutated Tuning/RolePrompts/Skills - // } - // return workflow.NewContinueAsNewError(ctx, OrchestratorWorkflow, nextInput) - // } -``` - -## 504 Learning Detail -```go -// Wrapping PrepareSkillsActivity for 504 learning: -for { - r := config.Tuning.PiRetry - ao := workflow.ActivityOptions{ - ScheduleToCloseTimeout: r.ScheduleToCloseTimeout, // 5m hard cap - StartToCloseTimeout: r.MaximumInterval, // per-attempt ceiling - RetryPolicy: &temporal.RetryPolicy{ - InitialInterval: r.InitialInterval, - BackoffCoefficient: r.BackoffCoefficient, - MaximumInterval: r.MaximumInterval, - NonRetryableErrorTypes: []string{"PiClientError"}, - }, - } - err := workflow.ExecuteActivity( - workflow.WithActivityOptions(ctx, ao), - action.PrepareSkillsActivity, - action.PrepareSkillsInput{Skills: config.Skills, StreamTimeout: r.StreamTimeout}, - ).Get(ctx, nil) - - var appErr *temporal.ApplicationError - if errors.As(err, &appErr) && appErr.Type() == "PiStreamTimeout" && r.StreamTimeout < r.StreamTimeoutMax { - config.Tuning.PiRetry.StreamTimeout = min(r.StreamTimeout*2, r.StreamTimeoutMax) - continue // ScheduleToCloseTimeout still bounds each attempt - } - if err != nil { - return OrchestratorOutput{}, err - } - break -} -``` - -## Verification -```bash -cd /Users/rockliang/workplace/Poimen/workflows -go test -v ./tests -run TestOrchestrator - -# Test file: tests/orchestrator_workflow_test.go -``` - -Test cases (mocked activities): -1. **Fan-out/fan-in:** Dispatch 3 tasks β†’ all complete β†’ results collected -2. **Squash-merge on complete:** All tasks pass β†’ GitSquashMergeActivity called -3. **Continue-as-new:** CycleCount reaches MaxCyclesBeforeCAN β†’ returns NewContinueAsNewError -4. **Signal mutation:** update-role-prompt signal β†’ next dispatch uses new prompt -5. **504 learning:** PrepareSkillsActivity returns PiStreamTimeout β†’ StreamTimeout doubled β†’ next PrepareSkillsActivity call uses doubled value, capped at Max - -## Done Criteria -- `go test ./tests -run TestOrchestrator` passes all 5 cases -- Signals mutate config without affecting in-flight TaskUnit -- Continue-as-new preserves OrchestratorConfig across cycles -- 504 learning loop doesn't exceed ScheduleToCloseTimeout diff --git a/tasks/T0.8.md b/tasks/T0.8.md deleted file mode 100644 index 49a4cd9..0000000 --- a/tasks/T0.8.md +++ /dev/null @@ -1,167 +0,0 @@ -# T0.8: Worker & Starter CLIs - -## Scope -Implement `cmd/worker/main.go`, `cmd/starter/main.go`, and `internal/config` for env-based loading. - -## Implementation - -### File: `internal/config/config.go` -```go -package config - -type TemporalConfig struct { - HostPort string // default: temporal.riotpiao.com:7233 - Namespace string // default: default - TLSCert string // env: TEMPORAL_TLS_CERT (file path) - TLSKey string // env: TEMPORAL_TLS_KEY (file path) -} - -type AppConfig struct { - Temporal AppConfig - AnthropicAPIKey string // env: ANTHROPIC_API_KEY -} - -func LoadConfig() (AppConfig, error) - // Read from env variables (TEMPORAL_*, ANTHROPIC_API_KEY) - // Return filled config -``` - -### File: `cmd/worker/main.go` -```go -func main() { - cfg, err := config.LoadConfig() - if err != nil { panic(err) } - - // Connect to Temporal - c, err := client.Dial(client.Options{ - HostPort: cfg.Temporal.HostPort, - Namespace: cfg.Temporal.Namespace, - // TLS options if provided - }) - if err != nil { panic(err) } - defer c.Close() - - // Create worker - w, err := worker.New(c, "default", worker.Options{}) - if err != nil { panic(err) } - - // Register all workflows - w.RegisterWorkflow(statemachine.OrchestratorWorkflow) - w.RegisterWorkflow(statemachine.TaskUnitWorkflow) - - // Register all activities - w.RegisterActivity(action.CloneRepoActivity) - w.RegisterActivity(action.GitWorktreeAddActivity) - w.RegisterActivity(action.GitCommitActivity) - w.RegisterActivity(action.GitPushActivity) - w.RegisterActivity(action.GitSquashMergeActivity) - w.RegisterActivity(action.PrepareSkillsActivity) - w.RegisterActivity(action.PlanningActivity) - w.RegisterActivity(action.ImplementerActivity) - w.RegisterActivity(action.JudgeActivity) - w.RegisterActivity(action.RunIntegrationTestActivity) - w.RegisterActivity(action.UpdateLessonsActivity) - w.RegisterActivity(action.ReadLessonsActivity) - - // Run worker - if err := w.Run(worker.InterruptCh()); err != nil { - panic(err) - } -} -``` - -### File: `cmd/starter/main.go` -```go -func main() { - var ( - repoPath = flag.String("repo", "", "target repo path") - remoteURL = flag.String("remote", "", "remote URL") - milestone = flag.String("milestone", "T0", "milestone ID") - dryRun = flag.Bool("dry-run", false, "disable git push/merge") - plannerModel = flag.String("planner-model", "claude-opus-5", "planner model ID") - judgeModel = flag.String("judge-model", "claude-opus-5", "judge model ID") - implementerModel = flag.String("implementer-model", "claude-sonnet-5", "implementer model ID") - ) - flag.Parse() - - cfg, err := config.LoadConfig() - if err != nil { panic(err) } - - // Connect to Temporal - c, err := client.Dial(client.Options{ - HostPort: cfg.Temporal.HostPort, - Namespace: cfg.Temporal.Namespace, - }) - if err != nil { panic(err) } - defer c.Close() - - // Build OrchestratorInput - input := statemachine.OrchestratorInput{ - TargetRepoPath: *repoPath, - RemoteURL: *remoteURL, - Milestone: *milestone, - Config: statemachine.OrchestratorConfig{ - SystemPrompt: "You are an expert software developer orchestrating multi-agent work.", - Skills: []statemachine.SkillRef{}, - RolePrompts: map[string]statemachine.PromptSpec{ - "planner": {TemplateRef: "planner/default.tmpl", Model: statemachine.ModelSpec{ModelID: *plannerModel, Thinking: "adaptive", Effort: "high"}}, - "judge": {TemplateRef: "judge/default.tmpl", Model: statemachine.ModelSpec{ModelID: *judgeModel, Thinking: "adaptive", Effort: "high"}}, - "implementer": {TemplateRef: "implementer/default.tmpl", Model: statemachine.ModelSpec{ModelID: *implementerModel}}, - }, - Tuning: statemachine.ActivityTuning{ - ImplementerBaseTimeout: 10 * time.Minute, - ImplementerMaxRetries: 3, - JudgeTimeout: 5 * time.Minute, - PiRetry: statemachine.PiRetryPolicy{ - ScheduleToCloseTimeout: 5 * time.Minute, - InitialInterval: 2 * time.Second, - MaximumInterval: 30 * time.Second, - BackoffCoefficient: 2.0, - StreamTimeout: 30 * time.Second, - StreamTimeoutMax: 2 * time.Minute, - }, - }, - }, - DryRun: *dryRun, - MaxCyclesBeforeCAN: 100, - } - - // Start workflow - workflowID := "orch-" + strings.ReplaceAll(*repoPath, "/", "-") - run, err := c.ExecuteWorkflow(context.Background(), client.StartWorkflowOptions{ID: workflowID, TaskQueue: "default"}, statemachine.OrchestratorWorkflow, input) - if err != nil { panic(err) } - - fmt.Printf("Started workflow %s\n", workflowID) - fmt.Printf("Monitor at: temporal.riotpiao.com:8080/namespaces/default/workflows/%s\n", workflowID) - - // Optionally wait for completion - // var result OrchestratorOutput - // err = run.Get(context.Background(), &result) -} -``` - -## Verification -```bash -cd /Users/rockliang/workplace/Poimen/workflows - -# Test build -go build ./cmd/worker -go build ./cmd/starter - -# Test worker registration (mock/local test): -go test -v ./tests -run TestWorkerRegistration - -# Manual test (requires temporal.riotpiao.com running): -# 1. Start worker: -go run ./cmd/worker & -# 2. In another terminal, start workflow: -go run ./cmd/starter --repo /tmp/fixture --remote file:///tmp/remote --dry-run -# 3. Check Temporal Web UI: should show workflow execution -``` - -## Done Criteria -- `go build ./cmd/worker` succeeds -- `go build ./cmd/starter` succeeds -- `go test ./tests -run TestWorkerRegistration` passes -- Manual test: `go run ./cmd/worker` connects to temporal.riotpiao.com:7233 without error (or test Temporal instance) -- Manual test: `go run ./cmd/starter --dry-run` returns workflow ID and URL immediately diff --git a/tasks/T0.9.md b/tasks/T0.9.md deleted file mode 100644 index 28fe51e..0000000 --- a/tasks/T0.9.md +++ /dev/null @@ -1,191 +0,0 @@ -# T0.9: End-to-End Test - -## Scope -Run against real `temporal.riotpiao.com` cluster + disposable forgejo scratch repo. All 7 verification items from PLAN.md. - -## Setup - -### Prerequisites -- `temporal.riotpiao.com` Temporal cluster accessible -- Forgejo instance running (for scratch repo) -- Local `git`, `go` 1.21+ -- `ANTHROPIC_API_KEY` env var set -- `TEMPORAL_NAMESPACE`, `TEMPORAL_TLS_CERT`, `TEMPORAL_TLS_KEY` env vars set if cluster requires them - -### Fixture Repo Structure -Create temporary fixture repo: -``` -/tmp/fixture/ - tasks/ - INDEX.md (copy from this repo) - board.md (minimal: 3 trivial tasks for quick run) -``` - -Minimal board.md: -``` -| T0.1 | Create file /tmp/fixture/output.txt with content "hello world" | [ ] | -| T0.2 | Create file /tmp/fixture/result.json with {"status": "ok"} | [ ] | -| T0.3 | Create file /tmp/fixture/done.txt with "COMPLETE" | [ ] | -``` - -## Run Sequence - -### 1. Clone & Fetch Bootstrap Test (T0.3 foundational) -```bash -cd /tmp -mkdir -p test-clone -go run ./cmd/starter \ - --repo /tmp/test-clone \ - --remote /tmp/fixture \ - --dry-run -# Check: /tmp/test-clone/.git exists after first run -# Check: Verify it's a valid git repo -``` - -### 2. Full Cycle with Dry-Run (no real push) -```bash -export FIXTURE_REMOTE=file:///tmp/fixture-remote-src -export FIXTURE_WORKTREE=/tmp/fixture-worktree - -# Start worker -go run ./cmd/worker & -WORKER_PID=$! - -# Start orchestrator workflow -go run ./cmd/starter \ - --repo /tmp/fixture \ - --remote file:///tmp/fixture-remote-src \ - --milestone T0 \ - --dry-run - -# Monitor Temporal Web UI: http://temporal.riotpiao.com:8080 -# Workflow ID: orch-tmp-fixture -# Expected: all 3 subtasks dispatched, Judge passes each, board updated, NO push to origin - -# Verify: -# - Board file shows all tasks marked done -# - No commits pushed to remote (because --dry-run) -# - Lessons file exists if any task was induced to fail - -kill $WORKER_PID -``` - -### 3. Live Signal Update Mid-Run -```bash -# Start same workflow again (different workflow ID) -go run ./cmd/starter \ - --repo /tmp/fixture \ - --remote file:///tmp/fixture-remote-src \ - --milestone T0.1 \ - --dry-run & -WF_ID=$! - -# While running, send update signal: -temporal workflow signal \ - --workflow-id \ - --name update-role-prompt \ - --input '{"role":"implementer","spec":{"template_ref":"implementer/default.tmpl","variables":{"marker":"from-signal"},...}}' - -# Check: next dispatched task includes "from-signal" in Variables -# Verify via board file or task output -``` - -### 4. 5xx Fault Injection (retry-then-succeed) -```bash -# Setup: Mock pi command to return 5xx first N times, then succeed -# (Use a local wrapper script or fault-injection proxy) - -# Run workflow: -go run ./cmd/starter \ - --repo /tmp/fixture \ - --remote file:///tmp/fixture-remote-src \ - --dry-run - -# Expected: -# - PrepareSkillsActivity retries with exponential backoff -# - Eventually succeeds after N retries -# - Workflow continues normally -``` - -### 5. 5xx Exhaustion (always-5xx, fail at 5m) -```bash -# Setup: Mock pi command to always return 503 - -# Run workflow (must have Implementer call PrepareSkillsActivity or similar pi-dependent step) - -# Expected: -# - PrepareSkillsActivity retries for ~5 minutes (ScheduleToCloseTimeout) -# - After 5m, activity fails -# - Workflow marks task as failed -# - Board reflects failure -``` - -### 6. 504 Stream Timeout Learning -```bash -# Setup: Mock pi command to return 504 - -# Run workflow: -go run ./cmd/starter --repo /tmp/fixture --remote file:///tmp/fixture-remote-src --dry-run - -# While running, query workflow state: -temporal workflow query \ - --workflow-id \ - --query-type current-config - -# Expected output includes config.Tuning.PiRetry.StreamTimeout (should be doubled from default 30s) -# After 504, next query shows it as 60s -# If 504 repeats, doubles again to 120s, capped at StreamTimeoutMax (2m) -``` - -### 7. Continue-as-New History Bound -```bash -# Run multiple cycles (manually via CLI or workflow logic) - -# Check Temporal Web UI: Workflow β†’ History tab -# Expected: -# - History is compact (not unbounded growth) -# - No duplication of events -# - Cycle count resets per continue-as-new -``` - -## Verification Checklist -- [ ] Fixture repo clones fresh when path empty -- [ ] Fetch-instead-of-clone on second run -- [ ] All 3 subtasks dispatched and complete -- [ ] Judge passes each task -- [ ] Board file updated with completion marks -- [ ] No push to origin when --dry-run -- [ ] Live signal (update-role-prompt) changes next dispatch -- [ ] Live signal (update-skills) re-preps skills exactly once -- [ ] 5xx retry-then-succeed: activity retries and eventually succeeds -- [ ] 5xx exhaustion: activity fails at ~5m mark, task marked failed -- [ ] 504 learning: StreamTimeout doubled and actually used on next attempt -- [ ] 504 learning: Stops doubling at StreamTimeoutMax (2m) -- [ ] Continue-as-new: History bounded, no unbounded growth -- [ ] Squash-merge result: One commit on main per submilestone (not yet, waiting for T0.1-T0.8 to pass first) - -## Done Criteria (All Must Pass) -1. All 7 checks in Verification Checklist marked `[x]` -2. No panics or unhandled errors in workflow execution -3. Temporal Web UI shows clean workflow execution with retries visible -4. Board file reflects accurate task completion state -5. Lessons file demonstrates learning across retries (if any failure induced) -6. Workflow completes within reasonable time (~10-30min for 3 subtasks + fault injection) - -## Cleanup -```bash -# Delete fixture remote and worktrees -rm -rf /tmp/fixture-remote-src /tmp/fixture-worktree - -# Kill any lingering worker processes -pkill -f "go run ./cmd/worker" - -# Optionally delete workflow from Temporal (if testing repeatedly) -temporal workflow delete --workflow-id orch-tmp-fixture -``` - -## Notes -- **Real forgejo remote:** The "disposable" remote can be on actual forgejo instance (`git@forgejo.homelab:test/workflows-e2e.git`), or a file:// URL locally -- **Anthropic API calls:** Use actual API (not mock) for real e2e; costs will be minimal if test tasks are simple -- **Temporal Web UI:** Set timezone to match your local time for easier log reading -- **Fault injection:** Can use `PATH` manipulation (wrapper scripts) or a local HTTP proxy (e.g., mitmproxy, Burp Suite) to inject 5xx/504 responses diff --git a/tasks/T1.1.md b/tasks/T1.1.md deleted file mode 100644 index 54d7216..0000000 --- a/tasks/T1.1.md +++ /dev/null @@ -1,263 +0,0 @@ -# T1.1: Workflow Error Recovery & Deadletter Handling - -**Submilestone:** T1 (Production Hardening) -**Status:** βœ… COMPLETE -**Branch:** `task/T1.1` - -## Overview - -Implement comprehensive error recovery, retry policies, deadletter handling, and state checkpointing for robust workflow execution with crash recovery capability. - -## Requirements - -### Retry Policies - -- Exponential backoff retry policies for different activity types -- Configurable initial interval, maximum interval, backoff coefficient, max attempts -- Three predefined policies: DefaultRetryPolicy, ActivityRetryPolicy, LLMActivityRetryPolicy -- LLM activities get more lenient retry settings (longer intervals, more attempts) -- Temporal SDK integration via `ToTemporalRetryPolicy()` - -### Deadletter Handling - -- Track permanently failed activities/tasks in a deadletter queue -- Persist deadletter items to JSON file for audit trail -- Mark items as recoverable or non-recoverable -- Support for batch retrieval of recoverable items -- Manual resolution/recovery notes on deadlettered items -- Clean audit trail with creation/update timestamps - -### State Checkpointing - -- Periodic checkpoint saving (configurable interval) -- Track workflow stages: clone, plan, implement, judge, merge -- Maintain lists of completed, pending, and failed tasks -- Persist checkpoints to JSON files for recovery -- Support resuming from latest checkpoint after crashes -- Metadata field for custom state tracking - -### Workflow Integration - -- Enhanced `OrchestratorWorkflowWithRecovery()` using recovery infrastructure -- Structured logging of all workflow progress -- Activity options include retry policies -- Track task lifecycle through checkpoint updates -- Graceful failure with deadletter fallback - -## Implementation - -### Internal Package: `internal/recovery` - -#### `retry.go` -- `RetryPolicy` struct with exponential backoff settings -- `DefaultRetryPolicy()` - 1s initial, 1m max, 2.0x backoff, 5 attempts -- `ActivityRetryPolicy()` - 2s initial, 5m max, 2.0x backoff, 3 attempts -- `LLMActivityRetryPolicy()` - 5s initial, 10m max, 1.5x backoff, 5 attempts -- `IsRetryableError()` - Determine if error should be retried -- `RetryCount` - Helper for manual retry tracking -- 8/8 unit tests passing βœ… - -#### `deadletter.go` -- `DeadletterItem` - Failed activity/task representation -- `DeadletterQueue` - Thread-safe queue with persistence -- Operations: Add, Get, GetAll, GetRecoverable, Remove, Resolve -- Automatic JSON persistence on every change -- Audit trail with CreatedAt/UpdatedAt timestamps -- 10/10 unit tests passing βœ… - -#### `checkpoint.go` -- `Checkpoint` - Workflow state snapshot -- `CheckpointManager` - Periodic checkpoint saving -- Track stages: clone, plan, implement, judge, merge -- Maintain task lists: completed, pending, failed -- Automatic periodic saving (configurable interval) -- Recovery support: resume from latest checkpoint -- Cleanup after successful completion -- 10/10 unit tests passing βœ… - -#### Unit Tests: `*_test.go` -- 40 tests total, all passing βœ… -- Comprehensive coverage of retry policies, deadletter operations, checkpoints -- Tests for persistence, recovery, edge cases - -### Workflow Integration - -**statemachine/orchestrator_recovery.go** -- `OrchestratorWorkflowWithRecovery()` demonstrates recovery patterns -- Uses `ActivityRetryPolicy()` for regular activities -- Uses `LLMActivityRetryPolicy()` for implementer activities -- Tracks success/failure for each task -- Structured logging at each step -- Graceful error handling with failure tracking -- Production-ready retry configuration - -**statemachine/types.go** -- Extended `ActivityTuning` with retry configuration fields: - - `InitialRetryInterval` - 2s default - - `MaxRetryInterval` - 5m default - - `RetryBackoffCoefficient` - 2.0 default - -## Verification Criteria - -βœ… **All criteria met:** - -1. **Retry Policies** - - Three pre-configured policies available - - Exponential backoff working correctly - - Integration with Temporal SDK tested - - 8/8 retry tests passing - -2. **Deadletter Handling** - - Items persist across crashes - - Thread-safe concurrent access - - Recoverable items identifiable - - Manual resolution with notes - - Audit trail maintained - - 10/10 deadletter tests passing - -3. **State Checkpointing** - - Periodic saving works - - Recovery from checkpoints tested - - Task state tracking (completed/pending/failed) - - Metadata support for extensions - - Cleanup after success - - 10/10 checkpoint tests passing - -4. **Workflow Integration** - - `OrchestratorWorkflowWithRecovery()` demonstrates patterns - - Structured logging at each step - - Proper error handling and tracking - - Compatible with existing Temporal infrastructure - -5. **Test Coverage** - - 40/40 recovery tests passing - - All core scenarios covered - - Edge cases handled - - Thread safety verified - -## Testing - -```bash -# Unit tests -go test -v ./internal/recovery -# Result: PASS (40/40 tests) - -# Full test suite -go test -v ./... -# Result: All tests pass - -# Testing recovery scenario -# 1. Start orchestrator with checkpointing -# 2. Kill workflow mid-way -# 3. Restart orchestrator -# 4. Verify resumption from checkpoint -# 5. Check deadlettered items for permanently failed tasks -``` - -## Kubernetes Integration - -With checkpoints and deadletter queue: - -```yaml -# Worker pod restarts automatically after crash -restartPolicy: Always - -# Health check ensures pod is ready -readinessProbe: - httpGet: - path: /health/ready - port: 8081 - -# Checkpoint directory mounted to persistent volume -volumeMounts: -- name: recovery - mountPath: /var/poimen/recovery - -volumes: -- name: recovery - persistentVolumeClaim: - claimName: poimen-recovery -``` - -## Configuration Example - -```go -// In starter command -recovery := recovery.NewCheckpointManager( - "/var/poimen/recovery", - 30*time.Second, // Checkpoint every 30s -) - -// Define retry policy for activities -tuning := statemachine.ActivityTuning{ - ImplementerBaseTimeout: 10 * time.Minute, - ImplementerMaxRetries: 3, - JudgeTimeout: 5 * time.Minute, - InitialRetryInterval: 2 * time.Second, - MaxRetryInterval: 5 * time.Minute, - RetryBackoffCoefficient: 2.0, -} -``` - -## Error Recovery Flow - -``` -Activity Execution - ↓ -[Success] β†’ Continue - ↓ -[Retryable Error] β†’ Apply RetryPolicy - β”œβ”€ Retry 1: Wait 2s, retry - β”œβ”€ Retry 2: Wait 4s, retry - β”œβ”€ Retry 3: Wait 8s, retry - └─ All retries exhausted - ↓ -[Add to Deadletter] β†’ CheckRecoverability - β”œβ”€ Recoverable: Mark for manual intervention - └─ Not Recoverable: Mark as permanently failed - ↓ -[Continue with remaining tasks] - ↓ -[Checkpoint State] β†’ Save to disk -``` - -## Files Changed - -- βœ… `internal/recovery/retry.go` - Retry policy framework (85 lines) -- βœ… `internal/recovery/retry_test.go` - Retry policy tests (52 lines) -- βœ… `internal/recovery/deadletter.go` - Deadletter queue (276 lines) -- βœ… `internal/recovery/deadletter_test.go` - Deadletter tests (170 lines) -- βœ… `internal/recovery/checkpoint.go` - State checkpointing (244 lines) -- βœ… `internal/recovery/checkpoint_test.go` - Checkpoint tests (174 lines) -- βœ… `statemachine/orchestrator_recovery.go` - Recovery patterns (251 lines) -- βœ… `statemachine/types.go` - Extended ActivityTuning -- βœ… `tasks/board-T1.md` - Task board update - -## Dependencies - -All internal, no new external dependencies added. - -## Key Design Decisions - -1. **Retry Policy Objects** - Immutable, composable, type-safe (not magic strings) -2. **Exponential Backoff** - Prevents thundering herd on repeated failures -3. **Deadletter Persistence** - JSON files for easy inspection and manual intervention -4. **Checkpoint Interval** - 30 seconds default (configurable) balances durability vs overhead -5. **Recoverable Flag** - Allows separation of transient vs permanent failures -6. **Thread Safety** - RWMutex on all concurrent structures -7. **Audit Trail** - CreatedAt/UpdatedAt on all persisted items - -## Next Steps (T1.3 β†’ T1.4 β†’ T1.5) - -1. **T1.3:** Activity timeout tuning automation based on historical failures -2. **T1.4:** Board state validation & auto-healing from corruption -3. **T1.5:** Workflow pause/resume with state snapshot - -## Notes - -- Checkpoints stored in `.poimen/recovery/checkpoints/` by default -- Deadletter queue stored in `.poimen/recovery/deadletters.json` by default -- Retry policies follow Temporal SDK conventions for compatibility -- All operations are thread-safe and designed for high concurrency -- Recovery infrastructure is independent of specific workflow implementation -- Can be extended to support custom recovery strategies via interfaces diff --git a/tasks/T1.2.md b/tasks/T1.2.md deleted file mode 100644 index 4bc6f89..0000000 --- a/tasks/T1.2.md +++ /dev/null @@ -1,223 +0,0 @@ -# T1.2: Structured Logging + Prometheus Metrics - -**Submilestone:** T1 (Production Hardening) -**Status:** βœ… COMPLETE -**Branch:** `task/T1.2` - -## Overview - -Implement structured JSON logging with zap and comprehensive Prometheus metrics export for observability. - -## Requirements - -### Structured Logging - -- Replace all `log.Printf` / `log.Fatalf` with structured logging -- Use `go.uber.org/zap` for structured JSON logging -- Support both development (colored) and production (JSON) modes -- Easy field attachment: `logging.Info("message", logging.String("key", "value"))` - -### Prometheus Metrics - -- 16 comprehensive metrics covering workflows, activities, LLM calls, git operations, judge decisions -- Counter metrics: workflow starts/completions, activity starts/completions, retries, LLM calls, git operations, judge decisions -- Histogram metrics: workflow duration, activity duration, LLM latency, git operation duration -- Gauge metrics: tasks in progress -- Error tracking: Temporal connection errors, cache hit/miss ratio -- Metrics exported on `/metrics` HTTP endpoint (Prometheus format) - -### Integration - -- Health check server (port 8081) now serves both `/health*` and `/metrics` -- Graceful logging shutdown with `logging.Sync()` -- Both worker and starter commands use structured logging - -## Implementation - -### Internal Package: `internal/logging` - -#### `logger.go` -- `InitLogger()` - Initialize global logger (dev or prod mode) -- `GetLogger()` - Get logger instance -- `Info()`, `Error()`, `Warn()`, `Debug()`, `Fatal()` - Log functions -- Field helpers: `String()`, `Int()`, `Int64()`, `Err()` -- `Sync()` - Flush buffered logs -- `With()` - Create logger with additional fields -- 8/8 unit tests passing βœ… - -#### `logger_test.go` -- Tests for logger initialization, field creation, logging functions -- Verifies no panics on concurrent logging - -### Internal Package: `internal/metrics` - -#### `metrics.go` -- 16 pre-registered Prometheus metrics -- Helper functions for recording each metric type -- Metrics organized by concern: workflows, activities, LLM, git, judge, temporal, cache -- 13/13 unit tests passing βœ… - -#### `metrics_test.go` -- Tests that all metrics are registered -- Tests that recording functions don't panic -- Verifies metric registration - -### Integration Points - -**cmd/worker/main.go** -- Initializes logger on startup -- Uses `logging.Info()`, `logging.Fatal()`, `logging.Warn()` throughout -- Health server serves `/metrics` endpoint -- Structured shutdown logging - -**cmd/starter/main.go** -- Initializes logger on startup -- Logs configuration load, Temporal connection, workflow start -- Supports `--health` command with structured logging -- Clean shutdown with `logging.Sync()` - -**internal/health/handler.go** -- Prometheus handler integrated via `promhttp.Handler()` -- `/metrics` endpoint available on all deployments - -## Verification Criteria - -βœ… **All criteria met:** - -1. **Structured logging deployed** - - All log statements use structured fields - - JSON output in production - - Colored output in development - -2. **Prometheus metrics exposed** - - 16 comprehensive metrics registered - - `/metrics` endpoint returns Prometheus text format - - Metrics include latencies, counters, and gauges - -3. **All metrics functional** - - `WorkflowExecutionsStarted` - workflow launch tracking - - `WorkflowExecutionsCompleted` - workflow completion with status - - `ActivityExecutionsStarted/Completed/Duration` - activity lifecycle - - `ActivityRetries` - retry tracking - - `LLMAPICallsTotal` / `LLMAPILatency` - LLM performance - - `GitOperationsTotal` / `GitOperationsDuration` - git operation tracking - - `TasksInProgress` - real-time task load - - `JudgeDecisionsTotal` - decision tracking - - `TemporalConnectionErrors` - error tracking - - `CacheHits` / `CacheMisses` - cache efficiency - -4. **Integration complete** - - Worker uses structured logging throughout - - Starter uses structured logging throughout - - Both commands can use `--health` to check system status - - Graceful shutdown flushes logs - -5. **Test coverage** - - 8/8 logging tests passing - - 13/13 metrics tests passing - - All unit tests pass - - No panics on concurrent logging - -## Testing - -```bash -# Unit tests -go test -v ./internal/logging ./internal/metrics -# Result: PASS (21/21 tests) - -# Full test suite -go test -v ./... -# Result: All tests pass - -# Integration test (requires running worker) -curl http://localhost:8081/metrics -# Returns: Prometheus metrics in text format - -# Logging output -ENVIRONMENT=development go run ./cmd/worker -# Output: Colored JSON logs with structured fields - -ENVIRONMENT=production go run ./cmd/worker -# Output: JSON logs suitable for Loki/ELK -``` - -## Kubernetes Configuration - -Example logging in pods: -```yaml -env: -- name: ENVIRONMENT - value: "production" -``` - -Example Prometheus scrape config: -```yaml -scrape_configs: -- job_name: 'poimen-worker' - static_configs: - - targets: ['localhost:8081'] - metrics_path: '/metrics' -``` - -## Metrics Schema - -All metrics prefixed with `poimen_`: - -### Workflow Metrics -- `poimen_workflow_executions_started_total{workflow_type}` - Counter -- `poimen_workflow_executions_completed_total{workflow_type, status}` - Counter -- `poimen_workflow_duration_seconds{workflow_type}` - Histogram - -### Activity Metrics -- `poimen_activity_executions_started_total{activity_type}` - Counter -- `poimen_activity_executions_completed_total{activity_type, status}` - Counter -- `poimen_activity_duration_seconds{activity_type}` - Histogram -- `poimen_activity_retries_total{activity_type}` - Counter - -### LLM Metrics -- `poimen_llm_api_calls_total{model_id, status}` - Counter -- `poimen_llm_api_latency_seconds{model_id}` - Histogram - -### Git Metrics -- `poimen_git_operations_total{operation, status}` - Counter -- `poimen_git_operations_duration_seconds{operation}` - Histogram - -### Other Metrics -- `poimen_tasks_in_progress{task_type}` - Gauge -- `poimen_judge_decisions_total{decision}` - Counter -- `poimen_temporal_connection_errors_total{error_type}` - Counter -- `poimen_cache_hits_total{cache_type}` - Counter -- `poimen_cache_misses_total{cache_type}` - Counter - -## Files Changed - -- βœ… `internal/logging/logger.go` - Structured logger (71 lines) -- βœ… `internal/logging/logger_test.go` - Logger tests (70 lines) -- βœ… `internal/metrics/metrics.go` - Prometheus metrics (222 lines) -- βœ… `internal/metrics/metrics_test.go` - Metrics tests (87 lines) -- βœ… `internal/health/handler.go` - Added `/metrics` endpoint -- βœ… `cmd/worker/main.go` - Structured logging integration -- βœ… `cmd/starter/main.go` - Structured logging integration -- βœ… `go.mod` - Added zap, prometheus/client_golang dependencies -- βœ… `tasks/board-T1.md` - Task board update - -## Dependencies Added - -- `go.uber.org/zap` v1.28.0 - Structured logging -- `github.com/prometheus/client_golang` v1.24.1 - Prometheus metrics -- Plus 8 transitive dependencies for Prometheus support - -## Next Steps (T1.1 β†’ T1.3 β†’ T1.4) - -1. **T1.1:** Workflow error recovery & deadletter handling -2. **T1.3:** Timeout tuning automation based on historical failures -3. **T1.4:** Board state validation & auto-heal from corruption - -## Notes - -- Logger uses global singleton pattern for simplicity (can be refactored to DI if needed) -- Metrics are auto-registered via `promauto` (thread-safe, idempotent) -- `/metrics` endpoint serves standard Prometheus text format (compatible with all scraping systems) -- Logging mode controlled by `ENVIRONMENT` env var (default: development) -- All metric labels are strings (Prometheus requirement) -- Histograms use default buckets (10ms, 100ms, 1s, 10s, etc.) diff --git a/tasks/T1.3.md b/tasks/T1.3.md deleted file mode 100644 index 703d878..0000000 --- a/tasks/T1.3.md +++ /dev/null @@ -1,371 +0,0 @@ -# T1.3: Activity Timeout Tuning Automation - -**Submilestone:** T1 (Production Hardening) -**Status:** βœ… COMPLETE -**Branch:** `task/T1.3` - -## Overview - -Implement intelligent timeout tuning system that learns from historical activity execution patterns and automatically recommends timeout adjustments to prevent failures and optimize performance. - -## Requirements - -### Timeout Analysis - -- Track activity execution metrics (duration, success/failure, timestamp) -- Calculate percentile metrics: P95, P99, max duration -- Identify patterns in timeout failures -- Generate confidence scores for recommendations -- Support percentile-based timeout recommendations (P99 + buffer) - -### Recommendation Engine - -- Analyze execution history to identify undertuned activities -- Recommend timeout increases when P99 exceeds current timeout -- Recommend timeout decreases when current timeout is excessive (>2x P99) -- Confidence scoring based on sample size and success rate -- Three priority levels: low (confidence <0.5), medium (0.5-0.7), high (>0.7) - -### Lessons Framework - -- Store timeout lessons in persistent JSONL files -- Track old timeout, new timeout, reason, failure rate -- Support per-task timeout lesson tracking -- Generate human-readable format for planner input -- Mark lessons as effective/ineffective for feedback loop - -### Signal Generation - -- Generate `TimeoutTuningSignal` objects for planner integration -- Include activity type, new timeout, reason, confidence -- Priority-based signaling (high-priority changes first) -- Compatible with existing lesson/signal framework - -## Implementation - -### Internal Package: `internal/tuning` - -#### `analyzer.go` -- `ExecutionMetric` - Recorded activity execution (type, duration, success, timestamp) -- `TimeoutRecommendation` - Analysis result with P95/P99, confidence, suggested timeout -- `TimeoutAnalyzer` - Core analyzer with metrics collection and analysis -- Methods: - - `RecordExecution()` - Record an activity execution - - `Analyze()` - Generate timeout recommendations - - `SaveMetrics()` / `LoadMetrics()` - Persistence to JSONL - - `SaveRecommendations()` - Save recommendations to JSON - - Helper functions for percentiles, averages, confidence calculation -- 14/14 unit tests passing βœ… - -#### `lessons.go` -- `TimeoutLesson` - A learned timeout adjustment -- `TimeoutLessonsStore` - Manage lessons for tasks -- `TimeoutTuningSignal` - Signal for planner to apply timeout change -- Methods: - - `AppendLesson()` - Record a lesson for a task - - `ReadLessons()` / `GetLatestLesson()` - Retrieve lessons - - `GenerateLessonFromRecommendation()` - Convert analysis to lesson - - `GenerateSignalsFromRecommendations()` - Create planner signals - - `FormatLessonsForPlanner()` - Human-readable format -- 22/22 unit tests passing βœ… - -#### Unit Tests: `*_test.go` -- 36 tests total, all passing βœ… -- Coverage of analysis, recommendations, lessons, signals -- Edge cases: empty metrics, all failures, multiple activities -- Persistence testing for metrics and lessons - -## Key Features - -### Intelligent Analysis - -```go -// Record metrics over time -analyzer.RecordExecution("implementer", 8*time.Second, true, nil) -analyzer.RecordExecution("implementer", 12*time.Second, true, nil) -analyzer.RecordExecution("implementer", 15*time.Second, false, err) - -// Analyze and get recommendations -currentTimeouts := map[string]time.Duration{"implementer": 5*time.Second} -recs, _ := analyzer.Analyze(currentTimeouts) -// Recommends: 5s β†’ ~20s (P99 + buffer) with 85% confidence -``` - -### Confidence Scoring - -- Sample confidence: More data = higher confidence (capped at 100 samples) -- Reliability confidence: 1.0 - failure_rate -- Weighted average: 40% sample + 60% reliability -- Example: 50 samples, 5% failure rate = 0.93 confidence - -### Lesson Tracking - -```go -// Persist lessons for task -lesson := &TimeoutLesson{ - ActivityType: "implementer", - OldTimeout: 5 * time.Second, - NewTimeout: 20 * time.Second, - Reason: "P99 duration 18s exceeded old timeout", - ConfidenceScore: 0.95, -} -store.AppendLesson("task-001", lesson) - -// Format for planner -formatted := FormatLessonsForPlanner(lessons) -// "Recent timeout lessons learned: -// [Lesson 1] implementer: -// Old Timeout: 5s β†’ New Timeout: 20s -// Reason: P99 duration 18s exceeded... -// Confidence: 95.0%" -``` - -### Signal Generation - -```go -// Generate signals from recommendations -signals := GenerateSignalsFromRecommendations(recommendations) -// Each signal includes: -// - ActivityType: "implementer" -// - NewTimeout: 20 * time.Second -// - Reason: "P99 exceeded" -// - Confidence: 0.95 -// - Priority: "high" (confidence > 0.7) -``` - -## Verification Criteria - -βœ… **All criteria met:** - -1. **Metrics Tracking** - - Recording works with success/failure - - Timestamps captured - - Error information stored - - 4 tests passing - -2. **Analysis Engine** - - P95/P99 calculation correct - - Confidence scoring reasonable - - Multiple activities handled - - Failure detection working - - 10 tests passing - -3. **Recommendation Generation** - - Undertuned timeouts identified - - Overtuned timeouts detected - - Confidence scores calculated - - Priority levels assigned - - 6 tests passing - -4. **Lesson Storage** - - JSONL persistence working - - Per-task lesson files - - Retrieval and formatting correct - - 16 tests passing - -5. **Integration Ready** - - Planner can read lessons - - Signals generated with correct structure - - Human-readable format - - File organization clear - -6. **Test Coverage** - - 36/36 tuning tests passing βœ… - - Edge cases covered - - Persistence tested - - Thread safety verified - -## Testing - -```bash -# Unit tests -go test -v ./internal/tuning -# Result: PASS (36/36 tests) - -# Full test suite -go test -v ./... -# Result: All tests pass - -# Integration test scenario -ta := NewTimeoutAnalyzer("/var/poimen") - -// Record metric data from past runs -for _, metric := range historicalMetrics { - ta.RecordExecution(metric.Activity, metric.Duration, metric.Success, metric.Error) -} - -// Get recommendations -recs, _ := ta.Analyze(currentTimeouts) -ta.SaveRecommendations(recs) - -// Generate lessons for planner -for _, rec := range recs { - lesson := GenerateLessonFromRecommendation(&rec) - store.AppendLesson("current-task", lesson) -} - -// Get signals for planner -signals := GenerateSignalsFromRecommendations(recs) -// Planner reads and applies: update-tuning signals -``` - -## Kubernetes Integration - -With timeout tuning: - -```yaml -# Activity metrics persisted in shared volume -volumeMounts: -- name: tuning - mountPath: /var/poimen/tuning - -# Recommendations available across pod restarts -volumes: -- name: tuning - persistentVolumeClaim: - claimName: poimen-tuning -``` - -## Configuration Example - -```go -// Initialize timeout analyzer -analyzer := tuning.NewTimeoutAnalyzer( - "/var/poimen/tuning", -) - -// Initialize lessons store -store := tuning.NewTimeoutLessonsStore( - "/var/poimen/tuning", -) - -// During workflow execution -for _, activity := range activities { - start := time.Now() - err := executeActivity(activity) - duration := time.Since(start) - - analyzer.RecordExecution( - activity.Type, - duration, - err == nil, - err, - ) -} - -// After milestone completion -recommendations, _ := analyzer.Analyze(currentActivityTimeouts) - -// Generate lessons for planner -for _, rec := range recommendations { - if rec.Confidence > 0.7 { // High confidence only - lesson := GenerateLessonFromRecommendation(&rec) - store.AppendLesson(taskID, lesson) - } -} - -// Save recommendations to disk -analyzer.SaveRecommendations(recommendations) - -// Planner can read and suggest timeout updates -lessons, _ := store.ReadLessons(taskID) -formatted := FormatLessonsForPlanner(lessons) -// Pass to planner as context for decision-making -``` - -## Timeout Tuning Algorithm - -``` -Analysis Pipeline - ↓ -[Collect Execution Metrics] - β”œβ”€ Duration (success and failure) - β”œβ”€ Success/failure count - └─ Timestamps - ↓ -[Calculate Statistics] - β”œβ”€ P95, P99 percentiles - β”œβ”€ Max duration - └─ Failure rate - ↓ -[Generate Recommendations] - β”œβ”€ Compare P99 + 20% buffer vs current timeout - β”œβ”€ Calculate confidence - β”‚ β”œβ”€ Sample confidence (n/100, capped at 1.0) - β”‚ β”œβ”€ Reliability confidence (1.0 - failure_rate) - β”‚ └─ Weighted: 0.4*sample + 0.6*reliability - └─ Assign priority (high/medium/low) - ↓ -[Store Lessons] - β”œβ”€ Save as JSONL per task - β”œβ”€ Track effectiveness - └─ Enable feedback loop - ↓ -[Generate Signals] - β”œβ”€ Create TimeoutTuningSignal objects - β”œβ”€ Include reason and confidence - └─ Ready for planner integration -``` - -## Files Changed - -- βœ… `internal/tuning/analyzer.go` - Timeout analysis engine (295 lines) -- βœ… `internal/tuning/analyzer_test.go` - Analyzer tests (220 lines) -- βœ… `internal/tuning/lessons.go` - Lesson storage and signals (175 lines) -- βœ… `internal/tuning/lessons_test.go` - Lesson tests (224 lines) -- βœ… `tasks/board-T1.md` - Task board update - -## Dependencies - -All internal, no new external dependencies added. - -## Key Design Decisions - -1. **Percentile-Based Timeout** - Uses P99 + 20% buffer (industry standard) -2. **Confidence Scoring** - Weighted combination of data quantity and reliability -3. **JSONL Persistence** - Human-readable, easy to debug, append-only -4. **Per-Task Lessons** - Enables targeted tuning for specific tasks -5. **Priority Signaling** - High-confidence changes promoted for planner attention -6. **Separation of Concerns** - Analyzer (metrics), Lessons (storage), Signals (integration) - -## Integration with Planner - -The planner can leverage timeout tuning: - -```go -// Planner initialization -lessons, _ := store.ReadLessons(taskID) -formattedLessons := FormatLessonsForPlanner(lessons) - -// Include in planner prompt context -systemPrompt := fmt.Sprintf( - "You are an expert planner. Previous lessons:\n%s\n...", - formattedLessons, -) - -// After planner suggests implementer, planner can suggest: -// "Signal: update-tuning(activity='implementer', newTimeout='20s')" -``` - -## Future Extensions - -- Activity dependency-aware timeouts -- Seasonal/periodic timeout adjustments -- ML-based timeout prediction -- SLO-aware timeout optimization -- Automatic circuit breaker thresholds - -## Next Steps (T1.4 β†’ T1.5 β†’ T1.6) - -1. **T1.4:** Board state validation & auto-healing -2. **T1.5:** Workflow pause/resume with state snapshots -3. **T1.6:** Comprehensive integration tests for concurrency - -## Notes - -- All metrics stored as JSONL (one per line) -- Recommendations stored as pretty JSON (easy to read) -- Lessons support feedback (can mark as effective/ineffective) -- Confidence range: 0.0-1.0 (0% to 100%) -- P99 + 20% buffer is conservative (safe overestimate) -- Works with any activity type (implementer, judge, git, etc.) diff --git a/tasks/T1.4.md b/tasks/T1.4.md deleted file mode 100644 index 07412ac..0000000 --- a/tasks/T1.4.md +++ /dev/null @@ -1,443 +0,0 @@ -# T1.4: Board State Validation & Auto-Healing - -**Submilestone:** T1 (Production Hardening) -**Status:** βœ… COMPLETE -**Branch:** `task/T1.4` - -## Overview - -Implement comprehensive board file validation and automatic corruption recovery to detect and fix inconsistencies between board file state and actual workflow state, preventing manual intervention and ensuring data integrity. - -## Requirements - -### Board Validation - -- Validate markdown structure (headers, table format) -- Check task ID format (T1.1, T1.2, etc.) -- Validate status fields ([x] or [ ]) -- Detect malformed rows and missing columns -- Generate detailed error and warning reports -- Parse task information from valid boards - -### Corruption Detection - -- Detect divergence between board file and actual task states -- Track state mismatches (expected vs actual) -- Support timestamp-based divergence tracking -- Identify missing or invalid task entries - -### Auto-Healing - -- Repair missing markdown headers -- Fix malformed status values -- Add missing table separators -- Correct invalid task IDs -- Heal divergences by syncing board with actual states -- Preserve task information during repairs - -### State Tracking - -- Persist actual task states to JSON -- Track task progression (pending β†’ in_progress β†’ completed/failed) -- Store task metrics alongside state -- Support multi-task concurrent state updates -- Generate statistics and completion reports - -## Implementation - -### Internal Package: `internal/board` - -#### `validator.go` -- `BoardValidationError` - Validation error with type, message, line number -- `BoardValidator` - Core validation and healing engine -- `TaskRow` - Parsed task from board file -- Methods: - - `ValidateBoard()` - Full board structure validation - - `ParseTasks()` - Extract tasks from valid boards - - `DetectDivergence()` - Find state mismatches - - `HealDivergence()` - Auto-fix state mismatches - - `RepairBoard()` - Fix structural issues - - Error/warning tracking and reporting -- 13/13 unit tests passing βœ… - -#### `state.go` -- `TaskState` - Actual task state (status, completion time, metrics) -- `StateTracker` - Manage actual task states -- Methods: - - `UpdateTaskState()` - Record task status change - - `GetTaskState()` / `GetAllStates()` - Retrieve states - - `GetCompletedTasks()` / `GetFailedTasks()` / `GetPendingTasks()` - Filter by status - - `AddMetric()` - Attach metrics to tasks - - `GetAsCompletionMap()` - Boolean map for comparison - - `GetStats()` / `GetLastUpdate()` - Analytics - - `Load()` - Persistence from JSON - - `Reset()` - Clear all state -- 16/16 unit tests passing βœ… - -#### Unit Tests: `*_test.go` -- 29 tests total, all passing βœ… -- Validator: parsing, validation, repair, divergence detection/healing -- State: tracking, filtering, persistence, metrics -- Integration: multi-task scenarios, state transitions - -## Key Features - -### Validation Pipeline - -``` -Board File Content - ↓ -[Check Structure] - β”œβ”€ Has title header - β”œβ”€ Has table separator - └─ Has task rows - ↓ -[Validate Each Task] - β”œβ”€ Valid task ID format (T#.# or T#) - β”œβ”€ Valid status ([x] or [ ]) - β”œβ”€ No missing columns - └─ Reasonable description - ↓ -[Report Results] - β”œβ”€ Errors (validation failed) - └─ Warnings (suspicious but valid) -``` - -### Corruption Healing - -```go -// Board has T1.1, T1.2, T1.3, T1.4 -// Actual states: T1.1=done, T1.2=done, T1.3=pending, T1.4=done -// Board shows: T1.1=done, T1.2=pending, T1.3=pending, T1.4=pending - -actualStates := map[string]bool{ - "T1.1": true, "T1.2": true, - "T1.3": false, "T1.4": true, -} - -divergences := validator.DetectDivergence(boardContent, actualStates) -// Finds: T1.2 (expected false, actual true), T1.4 (expected false, actual true) - -healed, changes := validator.HealDivergence(boardContent, actualStates) -// Fixes: Updates T1.2 and T1.4 status in board file -// Changes: ["Fixed T1.2: [ ] β†’ [x]", "Fixed T1.4: [ ] β†’ [x]"] -``` - -### State Tracking - -```go -// Initialize state tracker -tracker := NewStateTracker("/var/poimen") - -// Record task progress -tracker.UpdateTaskState("T1.1", "in_progress", "task/T1.1", nil) -tracker.AddMetric("T1.1", "lines_changed", 1247) -tracker.AddMetric("T1.1", "files_modified", 15) - -// Later, task completes -tracker.UpdateTaskState("T1.1", "completed", "task/T1.1", nil) - -// Query states -completed := tracker.GetCompletedTasks() // ["T1.1", ...] -stats := tracker.GetStats() -// {"total": 4, "counts": {"completed": 1, "pending": 3}} - -// Persist and recover -tracker.Load() // From disk -``` - -### Board Repair Examples - -``` -❌ BEFORE: Missing header -| T1.1 | Task | [x] | branch | verify | - -βœ… AFTER: Header added -# Task Board β€” Milestone T1: Production Hardening -| T1.1 | Task | [x] | branch | verify | - ---- - -❌ BEFORE: Invalid status -| T1.1 | Task | [?] | branch | verify | - -βœ… AFTER: Normalized -| T1.1 | Task | [ ] | branch | verify | - ---- - -❌ BEFORE: Missing separator -| ID | Scope | Status | Branch | -| T1.1 | Task | [x] | branch | - -βœ… AFTER: Separator added -| ID | Scope | Status | Branch | -|----|-------|--------|--------| -| T1.1 | Task | [x] | branch | -``` - -## Verification Criteria - -βœ… **All criteria met:** - -1. **Validation Engine** - - Detects missing headers - - Detects malformed tables - - Validates task IDs - - Validates status values - - Reports errors and warnings - - 13 tests passing - -2. **Corruption Detection** - - Identifies task divergences - - Tracks expected vs actual states - - Timestamps divergences - - Handles missing tasks - - 4 tests passing - -3. **Auto-Healing** - - Adds missing headers - - Fixes invalid status values - - Adds table separators - - Repairs divergent states - - Preserves data integrity - - 3 tests passing - -4. **State Management** - - Tracks task progression - - Stores completion timestamps - - Records failure information - - Supports metrics attachment - - Persists state to disk - - 16 tests passing - -5. **Integration** - - Works with actual board.md format - - Compatible with validation/tracking - - Supports concurrent updates - - Thread-safe operations - - 3 tests passing - -6. **Test Coverage** - - 29/29 board tests passing βœ… - - Edge cases covered - - Persistence tested - - Multi-task scenarios validated - -## Testing - -```bash -# Unit tests -go test -v ./internal/board -# Result: PASS (29/29 tests) - -# Full test suite -go test -v ./... -# Result: All tests pass - -# Integration scenario -validator := NewBoardValidator("repo/tasks") - -// Validate board -if !validator.ValidateBoard(boardContent) { - errors := validator.GetErrors() - // Fix: validator.RepairBoard(boardContent) -} - -// Parse tasks -tasks, _ := validator.ParseTasks(boardContent) -for _, task := range tasks { - // Track actual state - tracker.UpdateTaskState(task.ID, "completed", task.Branch, nil) -} - -// Detect divergence -tracker.Load() -actualStates := tracker.GetAsCompletionMap() -divergences := validator.DetectDivergence(boardContent, actualStates) - -// Heal if needed -if len(divergences) > 0 { - healed, changes := validator.HealDivergence(boardContent, actualStates) - // Save healed board - ioutil.WriteFile("tasks/board.md", []byte(healed), 0644) -} -``` - -## Kubernetes Integration - -With board healing: - -```yaml -# Board state persisted in shared volume -volumeMounts: -- name: board - mountPath: /var/poimen/board - -# State accessible across pod restarts -volumes: -- name: board - persistentVolumeClaim: - claimName: poimen-board - -# Liveness check includes board validation -livenessProbe: - exec: - command: - - /bin/sh - - -c - - | - validator validate /var/poimen/board/board.md || exit 1 -``` - -## Configuration Example - -```go -// Initialize validator and tracker -validator := NewBoardValidator("/var/poimen/board") -tracker := NewStateTracker("/var/poimen") - -// Load existing state from previous run -if err := tracker.Load(); err != nil { - log.Printf("Warning: could not load previous state: %v", err) -} - -// During workflow execution -boardContent, _ := ioutil.ReadFile("/var/poimen/board/board.md") - -// Validate board -if !validator.ValidateBoard(string(boardContent)) { - log.Printf("Board validation errors: %s", validator.ErrorSummary()) - - // Attempt repair - repaired, _ := validator.RepairBoard(string(boardContent)) - ioutil.WriteFile("/var/poimen/board/board.md", []byte(repaired), 0644) -} - -// Track task progress -for _, taskID := range tasksToRun { - tracker.UpdateTaskState(taskID, "in_progress", fmt.Sprintf("task/%s", taskID), nil) - - // ... execute task ... - - if taskSuccess { - tracker.UpdateTaskState(taskID, "completed", fmt.Sprintf("task/%s", taskID), nil) - } else { - tracker.UpdateTaskState(taskID, "failed", fmt.Sprintf("task/%s", taskID), taskErr) - } -} - -// Detect and heal divergence -actualStates := tracker.GetAsCompletionMap() -divergences := validator.DetectDivergence(string(boardContent), actualStates) - -if len(divergences) > 0 { - log.Printf("Detected %d divergences, healing...", len(divergences)) - healed, changes := validator.HealDivergence(string(boardContent), actualStates) - - for _, change := range changes { - log.Printf("Fixed: %s", change) - } - - ioutil.WriteFile("/var/poimen/board/board.md", []byte(healed), 0644) -} - -// Persist state for next run -_ = tracker.Load() -``` - -## Validation Algorithm - -``` -Board Validation - ↓ -[1] Check Presence - β”œβ”€ Has markdown header ("#") - └─ Has table separator ("---") - ↓ -[2] Find Task Table - β”œβ”€ Locate header row (| ID | ... |) - β”œβ”€ Skip separator - └─ Find first data row - ↓ -[3] Validate Each Row - β”œβ”€ Check column count - β”œβ”€ Validate task ID (T#.# format) - β”œβ”€ Validate status ([x] or [ ]) - └─ Warn on missing/empty fields - ↓ -[4] Generate Report - β”œβ”€ Collect all errors - β”œβ”€ Collect all warnings - └─ Return validation result (pass/fail) -``` - -## Healing Algorithm - -``` -Divergence Healing - ↓ -[1] Compare States - β”œβ”€ Board expected: [x] or [ ] - └─ Actual state: true or false - ↓ -[2] Find Mismatches - β”œβ”€ Board β‰  Actual: need fix - └─ Board = Actual: OK - ↓ -[3] Update Board - β”œβ”€ Replace [x] with [ ] or vice versa - β”œβ”€ Track changes made - └─ Preserve all other fields - ↓ -[4] Report Changes - β”œβ”€ List updated tasks - β”œβ”€ Show old β†’ new status - └─ Ready to write to disk -``` - -## Files Changed - -- βœ… `internal/board/validator.go` - Board validation and healing (378 lines) -- βœ… `internal/board/validator_test.go` - Validator tests (224 lines) -- βœ… `internal/board/state.go` - State tracking (195 lines) -- βœ… `internal/board/state_test.go` - State tests (229 lines) -- βœ… `tasks/board-T1.md` - Task board update - -## Dependencies - -All internal, no new external dependencies added. - -## Key Design Decisions - -1. **Separate Validator & Tracker** - Validation (format) vs State (semantics) -2. **JSON Persistence** - Human-readable, easy to inspect/debug -3. **Non-destructive Repairs** - Try to fix, report changes, allow rollback -4. **Detailed Error Reporting** - Line numbers, context, suggestions -5. **Thread-Safe State** - RWMutex for concurrent access -6. **Status Normalization** - [X] β†’ [x] for consistency - -## Future Extensions - -- Git integration: auto-commit healed boards -- Webhook notifications on divergence -- Historical divergence tracking -- Predictive healing (forecast issues) -- Multi-branch board tracking -- Board diffs and change logs - -## Next Steps (T1.5 β†’ T1.6 β†’ T1.7) - -1. **T1.5:** Workflow pause/resume with state snapshots -2. **T1.6:** Comprehensive integration tests for concurrency -3. **T1.7:** Audit logging (immutable decision log) - -## Notes - -- Board must have at least header and one task row -- Task IDs must match format: T# or T#.# -- Status values are case-insensitive during repair ([X] becomes [x]) -- Validation reports are detailed and actionable -- State tracking is optional (validator works standalone) -- Both validator and tracker are thread-safe -- Perfect for container/K8s environments with restart policies diff --git a/tasks/T1.5.md b/tasks/T1.5.md deleted file mode 100644 index 9fd7c65..0000000 --- a/tasks/T1.5.md +++ /dev/null @@ -1,434 +0,0 @@ -# T1.5: Workflow Pause/Resume with State Snapshots - -**Submilestone:** T1 (Production Hardening) -**Status:** βœ… COMPLETE -**Branch:** `task/T1.5` - -## Overview - -Implement workflow pause/resume capability with complete state serialization and recovery, enabling graceful pod restarts and mid-cycle workflow preservation without data loss. - -## Requirements - -### State Snapshots - -- Capture complete workflow state at any point in time -- Serialize all task metadata, metrics, configuration -- Persist snapshots to disk for recovery -- Track paused and resumed timestamps -- Support snapshot cleanup (after successful completion) - -### Pause Handling - -- Accept pause signals (manual or automatic) -- Save current workflow state before pausing -- Block workflow execution gracefully -- Prevent new activity starts while paused - -### Resume Handling - -- Accept resume signals after pod restart -- Restore workflow state from snapshots -- Continue execution from exact pause point -- Track resume attempts and success - -### Signal Management - -- PauseSignal with reason and grace period -- ResumeSignal with reason -- Channel-based signal reception (compatible with Temporal) -- Configurable timeout for pause/resume operations - -## Implementation - -### Internal Package: `internal/pause` - -#### `snapshot.go` -- `WorkflowSnapshot` - Complete workflow state capture -- `SnapshotManager` - Manage snapshots with persistence -- Methods: - - `CreateSnapshot()` - Capture current state - - `GetLatestSnapshot()` / `GetAllSnapshots()` - Retrieve snapshots - - `RestoreFromSnapshot()` - Load state for resumption - - `MarkResumed()` - Update snapshot after resumption - - `DeleteSnapshot()` - Cleanup after completion - - `ClearOldSnapshots()` - Batch cleanup by age - - `Load()` - Restore from disk - - `GetSnapshotStats()` - Analytics -- 16/16 unit tests passing βœ… - -#### `handler.go` -- `PauseSignal` - Pause request with reason and grace period -- `ResumeSignal` - Resume request with reason -- `PauseState` - Current pause/resume state -- `PauseHandler` - Orchestrate pause/resume operations -- Methods: - - `RequestPause()` / `RequestResume()` - Signal handling - - `IsPaused()` / `GetPauseState()` - State queries - - `WaitForPauseOrResume()` - Blocking wait with timeout - - `SaveSnapshot()` - Save state during pause - - `RestoreSnapshot()` - Load state during resume - - `ResetPauseState()` - Cleanup after completion - - `GetAllPauseStates()` / `GetPauseStats()` - Analytics -- 18/18 unit tests passing βœ… - -#### Unit Tests: `*_test.go` -- 34 tests total, all passing βœ… -- Snapshots: creation, persistence, recovery, cleanup -- Signals: pause/resume, state transitions, error handling -- Integration: concurrent workflows, multi-state transitions - -## Key Features - -### State Snapshot Structure - -```json -{ - "workflow_id": "orch-repo-path", - "timestamp": "2025-01-23T12:34:56Z", - "stage": "implement", - "completed_tasks": ["T1.1", "T1.2"], - "pending_tasks": ["T1.3", "T1.4"], - "failed_tasks": [], - "current_task_id": "T1.3", - "current_activity_id": "implementer-activity-123", - "task_metrics": { - "duration": 42.5, - "lines_modified": 1247 - }, - "workflow_metrics": { - "total_time": 300 - }, - "configuration": { - "timeout": 600, - "max_retries": 3 - }, - "paused_at": "2025-01-23T12:34:56Z", - "resumed_at": "2025-01-23T12:35:00Z" -} -``` - -### Pause/Resume Flow - -``` -Running Workflow - ↓ -[Pause Signal Received] - β”œβ”€ Save snapshot to disk - β”œβ”€ Block activity execution - └─ Wait for pause acknowledgment - ↓ -[Pod Restarts] - ↓ -[Resume Signal Sent] - β”œβ”€ Load snapshot from disk - β”œβ”€ Restore all state - └─ Continue from exact point - ↓ -Workflow Resumes -``` - -### Usage Example - -```go -// Initialize pause infrastructure -snapshotMgr := pause.NewSnapshotManager("/var/poimen") -pauseHandler := pause.NewPauseHandler(snapshotMgr) - -// During workflow execution -// ... tasks executing ... -if isPauseRequested { - // Save state before pausing - snapshot, _ := pauseHandler.SaveSnapshot( - "orch-task-1", - "implement", - []string{"T1.1", "T1.2"}, // completed - []string{"T1.3", "T1.4"}, // pending - []string{}, // failed - "T1.3", // current - "activity-123", - taskMetrics, - workflowMetrics, - configuration, - ) - - // Handle pause signal - pauseHandler.RequestPause(&pause.PauseSignal{ - WorkflowID: "orch-task-1", - Reason: "pod restart", - RequestedAt: time.Now(), - }) - - // Wait for actual pause (with timeout) - _ = pauseHandler.WaitForPauseOrResume("orch-task-1", 5*time.Second) - // Pod restarts here -} - -// On resume -if pauseHandler.HasSnapshot("orch-task-1") { - // Restore state - snapshot, _ := pauseHandler.RestoreSnapshot("orch-task-1") - - // Resume signal - pauseHandler.RequestResume(&pause.ResumeSignal{ - WorkflowID: "orch-task-1", - Reason: "pod restarted", - RequestedAt: time.Now(), - }) - - // Continue execution from restored state - restoreTasks(snapshot.PendingTasks) - executeFrom(snapshot.CurrentTaskID) -} - -// After workflow completes -pauseHandler.ResetPauseState("orch-task-1") -``` - -## Verification Criteria - -βœ… **All criteria met:** - -1. **State Snapshots** - - Complete state captured (tasks, metrics, configuration) - - Persisted to disk (JSON format) - - Retrieved correctly - - Timestamps tracked (paused_at, resumed_at) - - 16 tests passing - -2. **Pause Handling** - - Pause signal accepted - - State saved before pausing - - Workflow blocks during pause - - Multiple workflows can be paused - - 10 tests passing - -3. **Resume Handling** - - Resume signal accepted - - State restored correctly - - Workflow continues from exact point - - Timestamps updated - - 8 tests passing - -4. **Signal Management** - - PauseSignal with reason/grace period - - ResumeSignal with reason - - Channel-based signal reception - - Configurable timeouts - - Error handling - - 10 tests passing - -5. **Snapshot Recovery** - - Snapshots load from disk - - Old snapshots can be cleaned up - - Multiple snapshots managed - - Stats available - - 16 tests passing - -6. **Test Coverage** - - 34/34 pause/resume tests passing βœ… - - Edge cases covered (resume without pause, nil signals, timeouts) - - Concurrent workflows tested - - State transitions verified - -## Testing - -```bash -# Unit tests -go test -v ./internal/pause -# Result: PASS (34/34 tests) - -# Full test suite -go test -v ./... -# Result: All tests pass - -# Integration scenario -// Simulate pause/resume cycle -sm := pause.NewSnapshotManager("/var/poimen") -ph := pause.NewPauseHandler(sm) - -// Save snapshot before pause -ph.SaveSnapshot( - "wf-1", "implement", - []string{"T1.1"}, []string{"T1.2"}, nil, - "T1.2", "activity-1", - nil, nil, nil, -) - -// Pause -ph.RequestPause(&pause.PauseSignal{WorkflowID: "wf-1"}) - -// Verify paused -assert.True(t, ph.IsPaused("wf-1")) - -// Resume -ph.RequestResume(&pause.ResumeSignal{WorkflowID: "wf-1"}) -assert.False(t, ph.IsPaused("wf-1")) - -// Restore -snapshot, _ := ph.RestoreSnapshot("wf-1") -assert.Equal(t, "implement", snapshot.Stage) -``` - -## Kubernetes Integration - -With pause/resume: - -```yaml -# Workflow pod restarts gracefully -terminationGracePeriodSeconds: 30 - -# Pre-stop hook saves state and signals pause -lifecycle: - preStop: - exec: - command: ["/bin/sh", "-c", "pkill -SIGTERM orchestrator"] - -# State persisted in shared volume -volumeMounts: -- name: pause-state - mountPath: /var/poimen/snapshots - -volumes: -- name: pause-state - persistentVolumeClaim: - claimName: poimen-pause-state - -# Startup hook detects and restores from snapshot -postStart: - exec: - command: ["/bin/sh", "-c", "if [ -f /var/poimen/snapshots/$(WORKFLOW_ID).snapshot.json ]; then /app/orchestrator --resume; fi"] -``` - -## Configuration Example - -```go -// Initialize with custom base path -snapshotMgr := pause.NewSnapshotManager("/data/poimen/pause") - -// Create pause handler -pauseHandler := pause.NewPauseHandler(snapshotMgr) - -// Load existing snapshots from disk -_ = snapshotMgr.Load() - -// Handle pause request -pauseHandler.RequestPause(&pause.PauseSignal{ - WorkflowID: workflowID, - Reason: "graceful shutdown", - RequestedAt: time.Now(), - GracePeriod: 30 * time.Second, -}) - -// Wait for pause to complete -isPaused, err := pauseHandler.WaitForPauseOrResume(workflowID, 60*time.Second) - -// Handle resume after restart -if pauseHandler.HasSnapshot(workflowID) { - snapshot, _ := pauseHandler.RestoreSnapshot(workflowID) - - // Resume workflow from exact point - executeWorkflow(snapshot) -} -``` - -## Storage Layout - -``` -/var/poimen/ -β”œβ”€β”€ snapshots/ -β”‚ β”œβ”€β”€ orch-task-1.snapshot.json -β”‚ β”œβ”€β”€ orch-task-2.snapshot.json -β”‚ └── orch-task-3.snapshot.json -└── pause-state/ - └── (managed by PauseHandler) -``` - -## Files Changed - -- βœ… `internal/pause/snapshot.go` - Snapshot management (251 lines) -- βœ… `internal/pause/snapshot_test.go` - Snapshot tests (227 lines) -- βœ… `internal/pause/handler.go` - Pause/resume handler (224 lines) -- βœ… `internal/pause/handler_test.go` - Handler tests (274 lines) -- βœ… `tasks/board-T1.md` - Task board update - -## Dependencies - -All internal, no new external dependencies added. - -## Key Design Decisions - -1. **Separate Manager & Handler** - Snapshots (storage) vs Signals (orchestration) -2. **JSON Persistence** - Human-readable, debuggable snapshots -3. **Channel-Based Signaling** - Compatible with Temporal SDK patterns -4. **Complete State Capture** - Tasks, metrics, configuration all included -5. **Non-Destructive Pause** - Snapshot saved before pause, can be cleaned up later -6. **Configurable Timeout** - Flexible pause duration handling -7. **Thread-Safe Operations** - RWMutex for concurrent access - -## Pause/Resume Algorithm - -``` -Pause Flow - ↓ -[1] Receive Pause Signal - β”œβ”€ Record workflow ID and reason - └─ Set grace period - ↓ -[2] Save Snapshot - β”œβ”€ Capture all task state - β”œβ”€ Record metrics/config - └─ Persist to JSON file - ↓ -[3] Block Execution - β”œβ”€ Set IsPaused flag - β”œβ”€ Notify channels - └─ Wait for acknowledgment - ↓ -[4] Pod Restart - └─ Snapshot persists on disk - -Resume Flow - ↓ -[1] Pod Restarted - β”œβ”€ Load snapshots from disk - └─ Check for paused workflows - ↓ -[2] Receive Resume Signal - β”œβ”€ Record workflow ID and reason - └─ Mark ResumedAt timestamp - ↓ -[3] Restore Snapshot - β”œβ”€ Load from disk - β”œβ”€ Restore all state - └─ Return to caller - ↓ -[4] Continue Execution - β”œβ”€ Execute remaining tasks - └─ Update metrics as normal -``` - -## Future Extensions - -- Snapshot compression for large workflows -- Incremental snapshots (only changed state) -- Cross-pod snapshot sharing -- Snapshot encryption for sensitive data -- Snapshot versioning and rollback -- Activity-level state checkpoints -- Automatic pause on resource limits - -## Next Steps (T1.6 β†’ T1.7) - -1. **T1.6:** Comprehensive integration tests for concurrency -2. **T1.7:** Audit logging (immutable decision log) - -## Notes - -- Snapshots identified by workflow ID -- Paused workflows can be resumed from any pod -- Snapshot cleanup is manual (via DeleteSnapshot or ClearOldSnapshots) -- Multiple workflows can be paused concurrently -- Pause handler is thread-safe for concurrent signal handling -- Compatible with Temporal workflow signals pattern -- Perfect for Kubernetes rolling updates and graceful shutdowns diff --git a/tasks/T1.8.md b/tasks/T1.8.md deleted file mode 100644 index ab904f5..0000000 --- a/tasks/T1.8.md +++ /dev/null @@ -1,174 +0,0 @@ -# T1.8: Health Checks for Kubernetes - -**Submilestone:** T1 (Production Hardening) -**Status:** βœ… COMPLETE -**Branch:** `task/T1.8` - -## Overview - -Implement comprehensive health checks for Kubernetes deployments with liveness and readiness probes. - -## Requirements - -### Endpoints - -- **GET /health** - Full health report (JSON) - - Returns 200 if healthy, 503 if unhealthy - - Includes all component statuses, latencies, timestamps - -- **GET /health/live** - Kubernetes liveness probe - - Returns 200 if service is running - - Returns 503 if not initialized - -- **GET /health/ready** - Kubernetes readiness probe - - Returns 200 if service is ready to accept traffic - - Returns 503 if any component unhealthy - -### Components - -1. **Temporal** - Cluster connectivity check - - Attempts to get a workflow execution - - Returns healthy if Temporal responds (even with NotFound) - - Returns unhealthy if unreachable - -### Features - -- Periodic health check caching (30s interval) to avoid excessive checks -- JSON health reports with component status, latency, timestamp -- Separate liveness and readiness checks for K8s probes -- Graceful shutdown with health server cleanup - -## Implementation - -### Internal Package: `internal/health` - -#### `health.go` -- `Status` type with constants: `StatusHealthy`, `StatusUnhealthy`, `StatusUnknown` -- `ComponentHealth` struct for individual component status -- `HealthReport` struct for complete health status -- `Checker` interface for health checking -- `Check()` method that performs comprehensive health check -- `IsHealthy()` for quick boolean check -- Caching mechanism to avoid repeated checks within interval - -#### `handler.go` -- HTTP handler implementation -- `RegisterRoutes()` to set up endpoints on a mux -- Handlers for `/health`, `/health/live`, `/health/ready` -- Proper HTTP status codes (200 for healthy, 503 for unhealthy) - -#### `health_test.go` -- Unit tests for health checker -- Tests for nil client, caching, JSON serialization -- Tests for timestamp validation -- 10/10 tests passing βœ… - -### Integration - -**cmd/worker/main.go** -- Health check server runs on port 8081 -- Runs in separate goroutine alongside worker -- Graceful shutdown on SIGINT/SIGTERM -- Waits for health server to shutdown before exiting - -**cmd/starter/main.go** -- `--health` flag to run health check and exit -- Outputs JSON health report -- Returns non-zero exit code if unhealthy - -## Verification Criteria - -βœ… **All criteria met:** - -1. **Health endpoints responsive** - - GET /health returns 200 with JSON report - - GET /health/live returns 200 if running - - GET /health/ready returns 503 if Temporal unavailable - -2. **Kubernetes integration** - - Can be used as livenessProbe target - - Can be used as readinessProbe target - - Port 8081 exposed for probes - -3. **Component checks** - - Temporal connectivity verified via GetWorkflow call - - Caching prevents excessive health checks - - Latency measured and reported - -4. **Graceful shutdown** - - Health server stops on SIGINT/SIGTERM - - Worker stops cleanly - - No hanging goroutines - -5. **CLI integration** - - `starter --health` command works - - Outputs JSON report - - Exits with appropriate code - -## Testing - -```bash -# Unit tests -go test -v ./internal/health -# Result: PASS (10/10 tests) - -# Integration test (requires Temporal) -# When Temporal unavailable: -curl http://localhost:8081/health -# Returns: 503 with status="unhealthy", components.temporal.error set - -# When Temporal available: -curl http://localhost:8081/health -# Returns: 200 with status="healthy" -``` - -## Kubernetes Configuration - -Example liveness probe: -```yaml -livenessProbe: - httpGet: - path: /health/live - port: 8081 - initialDelaySeconds: 10 - periodSeconds: 10 -``` - -Example readiness probe: -```yaml -readinessProbe: - httpGet: - path: /health/ready - port: 8081 - initialDelaySeconds: 5 - periodSeconds: 5 -``` - -## Files Changed - -- βœ… `internal/health/health.go` - Core health checker (106 lines) -- βœ… `internal/health/handler.go` - HTTP endpoints (68 lines) -- βœ… `internal/health/health_test.go` - Unit tests (119 lines) -- βœ… `cmd/worker/main.go` - Worker integration -- βœ… `cmd/starter/main.go` - Starter health check command -- βœ… `tasks/board-T1.md` - Task board update - -## Dependencies - -- `go.temporal.io/sdk/client` - Already in go.mod -- `net/http` - Standard library -- `encoding/json` - Standard library -- `github.com/stretchr/testify/assert` - Already in go.mod - -## Notes - -- Health check server runs on `:8081` (separate from main application) -- Caching interval set to 30 seconds (configurable) -- Temporal check uses GetWorkflow with timeout for quick response -- Handler is reusable across different services - -## Next Steps (T1.7 β†’ T1.1 β†’ T1.2) - -1. **T1.7:** Immutable audit logging (track all decisions) -2. **T1.2:** Structured logging + Prometheus metrics -3. **T1.1:** Workflow error recovery & deadletter handling diff --git a/tasks/board-T1.md b/tasks/board-T1.md deleted file mode 100644 index d447969..0000000 --- a/tasks/board-T1.md +++ /dev/null @@ -1,19 +0,0 @@ -# Task Board β€” Milestone T1: Production Hardening - -**Submilestone:** T1 (Error recovery, observability, metrics, reliability) - -| ID | Scope | Status | Branch | Verification | -|----|-------|--------|--------|--------------| -| T1.1 | Workflow error recovery: retry policies, deadletter handling, graceful shutdown | [x] | `task/T1.1` | Simulate orchestrator crash mid-cycle, resume without data loss | -| T1.2 | Structured logging + metrics export (Prometheus/OpenTelemetry integration) | [x] | `task/T1.2` | Metrics visible in homelab Grafana, logs queryable in Loki | -| T1.3 | Activity timeout tuning automation: learn from historical failures, recommend overrides | [x] | `task/T1.3` | Planner reads lessons file, suggests `update-tuning` signal based on patterns | -| T1.4 | Board state validation: detect corruption, auto-heal from board divergence | [x] | `task/T1.4` | Corrupt board file recovered without manual intervention | -| T1.5 | Workflow pause/resume with state snapshot: serialize mid-cycle state to persistent store | [x] | `task/T1.5` | Pause signal, restart pod, resume signal β†’ workflow continues from exact point | -| T1.6 | Comprehensive integration tests: multi-pod concurrency, network flakiness simulation | [x] | `task/T1.6` | Concurrent orchestrator instances on shared repo pass e2e without conflicts | -| T1.7 | Audit logging: all planner decisions, judge verdicts, implementer changes logged immutably | [x] | `task/T1.7` | Audit log persists across workflow restarts, queryable by task/timestamp | -| T1.8 | Health checks: Temporal connectivity, git repo accessibility, LLM API availability | [x] | `task/T1.8` | Periodic health probes, liveness/readiness endpoints for K8s | - ---- - -## Submission Criteria -All T1.1–T1.8 marked `[x]` β†’ submilestone complete β†’ squash-merge `task/T1.*` to main. diff --git a/tasks/board-T2.md b/tasks/board-T2.md deleted file mode 100644 index 66d25d0..0000000 --- a/tasks/board-T2.md +++ /dev/null @@ -1,19 +0,0 @@ -# Task Board β€” Milestone T2: Scale & Performance - -**Submilestone:** T2 (Distributed execution, caching, performance optimization) - -| ID | Scope | Status | Branch | Verification | -|----|-------|--------|--------|--------------| -| T2.1 | Activity result caching: deduplicate repeated LLM calls for same task state | [x] | `task/T2.1` | Implementer called 2x on same code β†’ second call returns cached Implementer output | -| T2.2 | Parallel task dispatch: multiple T0.x tasks execute truly concurrently (not sequential) | [x] | `task/T2.2` | 9 tasks complete in ~1/9 total time (wall-clock speedup measured) | -| T2.3 | Prompt template caching: pre-compile Go templates on worker startup | [x] | `task/T2.3` | Template render latency < 100ms (vs parse+render each time) | -| T2.4 | Lessons file indexing: fast lookup of past failures without full file scan | [x] | `task/T2.4` | Query lessons by task type β†’ return in < 10ms for 1000s of entries | -| T2.5 | Git operation batching: combine multiple worktree commits into single push/merge | [x] | `task/T2.5` | N tasks β†’ 1 push (vs N pushes), measured via git ref-log | -| T2.6 | LLM request batching: group similar Implementer calls into one API request | [x] | `task/T2.6` | 3 implementer tasks β†’ 1 Anthropic API call with batch input (vs 3 separate calls) | -| T2.7 | Workflow history pruning: trim old task unit outputs from orchestrator history | [x] | `task/T2.7` | Continue-as-new cycle history size constant despite 1000s of task units completed | -| T2.8 | Distributed lock optimization: replace flock with Redis/etcd for multi-pod scenarios | [x] | `task/T2.8` | 5 concurrent orchestrators on different pods share FS safely via distributed lock | - ---- - -## Submission Criteria -All T2.1–T2.8 marked `[x]` β†’ submilestone complete β†’ squash-merge `task/T2.*` to main. diff --git a/tasks/board-T3.md b/tasks/board-T3.md deleted file mode 100644 index 14cc762..0000000 --- a/tasks/board-T3.md +++ /dev/null @@ -1,19 +0,0 @@ -# Task Board β€” Milestone T3: Feature Expansion - -**Submilestone:** T3 (Custom plugins, workflow templates, audit, advanced features) - -| ID | Scope | Status | Branch | Verification | -|----|-------|--------|--------|--------------| -| T3.1 | Custom skill plugins: load user-defined skills from plugin registry (not just pi clone) | [x] | `task/T3.1` | Custom skill plugin loads, PrepareSkillsActivity calls plugin:// URLs | -| T3.2 | Workflow templates: save/load orchestrator config as YAML templates (not CLI flags only) | [x] | `task/T3.2` | Load template `templates/golang-project.yaml` β†’ workflow configures Planner/Judge/Implementer for Go projects | -| T3.3 | Task dependency graph: specify task order (T0.2 must complete before T0.3 can start) | [x] | `task/T3.3` | Board supports `depends_on: [T0.1]` field, orchestrator respects ordering | -| T3.4 | Human-in-the-loop gates: pause workflow, require approval before proceeding to next task | [x] | `task/T3.4` | Workflow waits for `approve-task` signal, Judge verdict is final (can't auto-retry after user approval) | -| T3.5 | Custom Judge implementations: swap default Judge for domain-specific validator | [x] | `task/T3.5` | Register custom JudgeActivity, orchestrator uses it instead of default | -| T3.6 | Immutable audit trail: all Planner/Judge/Implementer decisions written to tamper-proof log | [x] | `task/T3.6` | Audit log signed with per-workflow key, verification prevents tampering | -| T3.7 | Workflow composition: nest OrchestratorWorkflows (one orchestrator dispatches child orchestrators) | [x] | `task/T3.7` | Multi-level task hierarchy: T0 milestone β†’ T0.a/T0.b sub-milestones, each with own orchestrator | -| T3.8 | Integration with external task systems: import tasks from Linear, GitHub Issues, JIRA | [x] | `task/T3.8` | Load board from GitHub Issues API, update issues with task completion status | - ---- - -## Submission Criteria -All T3.1–T3.8 marked `[x]` β†’ submilestone complete β†’ squash-merge `task/T3.*` to main. diff --git a/tasks/board-T4.md b/tasks/board-T4.md deleted file mode 100644 index e89e0cb..0000000 --- a/tasks/board-T4.md +++ /dev/null @@ -1,128 +0,0 @@ -# T4: Advanced Operations & Analytics - -## Overview -Advanced operational capabilities for monitoring, visualization, cost optimization, and multi-cluster orchestration. Builds on T1-T3 foundation to enable enterprise-scale deployment. - -## Tasks - -| Task | Description | Tests | Status | -|------|-------------|-------|--------| -| T4.1 | Real-time metrics dashboard: queryable Prometheus metrics with aggregation | [ ] | πŸ”œ TODO | -| T4.2 | Workflow visualization & DAG rendering: browser-based workflow inspector | [ ] | πŸ”œ TODO | -| T4.3 | Advanced search & filtering: Elasticsearch-like task/workflow search | [ ] | πŸ”œ TODO | -| T4.4 | Cost tracking & optimization: LLM API, git push, compute resource costs | [ ] | πŸ”œ TODO | -| T4.5 | Automated alerting & anomaly detection: threshold rules, ML-based anomalies | [ ] | πŸ”œ TODO | -| T4.6 | Workflow profiling & bottleneck analysis: identify slowest tasks | [ ] | πŸ”œ TODO | -| T4.7 | Multi-cluster orchestration: deploy orchestrators across K8s clusters | [ ] | πŸ”œ TODO | -| T4.8 | Self-deployment: orchestrator deploys itself (meta!) | [ ] | πŸ”œ TODO | - ---- - -## Implementation Plan - -### T4.1: Real-time Metrics Dashboard -- `internal/dashboard/metrics_aggregator.go` - Query Prometheus for metrics -- `internal/dashboard/metrics_aggregator_test.go` - 15 tests -- Features: - - Aggregate gauge/counter/histogram metrics - - Time-range queries - - Percentile calculations (p50, p95, p99) - - Error rate aggregation - - Throughput calculations - -### T4.2: Workflow Visualization -- `internal/visualization/dag_renderer.go` - DAG graph generation -- `internal/visualization/dag_renderer_test.go` - 12 tests -- Features: - - Convert dependency graph to DOT format - - SVG/PNG rendering capability - - Task status coloring - - Critical path highlighting - - Parallel task grouping - -### T4.3: Advanced Search & Filtering -- `internal/search/workflow_search.go` - Full-text search -- `internal/search/workflow_search_test.go` - 18 tests -- Features: - - Index workflows by content - - Filter by status, date, assignee - - Full-text search on task descriptions - - Regex pattern matching - - Saved filters - -### T4.4: Cost Tracking & Optimization -- `internal/cost/cost_tracker.go` - Track compute/API costs -- `internal/cost/cost_tracker_test.go` - 16 tests -- Features: - - LLM API call costs (tokens Γ— price) - - Git push operation costs - - K8s compute resource costs - - Cost per workflow - - Cost optimization recommendations - -### T4.5: Automated Alerting & Anomaly Detection -- `internal/alerting/alert_manager.go` - Rule-based alerts -- `internal/alerting/alert_manager_test.go` - 20 tests -- Features: - - Threshold-based alerts - - Pattern-based anomaly detection - - Alert routing (email, Slack, PagerDuty) - - Alert history - - Deduplication - -### T4.6: Workflow Profiling & Bottleneck Analysis -- `internal/profiling/workflow_profiler.go` - Identify slow tasks -- `internal/profiling/workflow_profiler_test.go` - 17 tests -- Features: - - Per-task execution time breakdown - - Critical path identification - - Parallel vs sequential timing - - Resource utilization per task - - Optimization suggestions - -### T4.7: Multi-cluster Orchestration -- `internal/clusters/cluster_manager.go` - Manage multiple K8s clusters -- `internal/clusters/cluster_manager_test.go` - 19 tests -- Features: - - Register/discover clusters - - Route workflows to clusters - - Cross-cluster task coordination - - Cluster health monitoring - - Failover support - -### T4.8: Self-Deployment -- `internal/deployment/self_deployer.go` - Orchestrator deploys itself -- `internal/deployment/self_deployer_test.go` - 14 tests -- Features: - - Build orchestrator container - - Generate K8s manifests - - Deploy new version - - Health check & rollback - - Version management - ---- - -## Test Coverage Target -- T4 Total: **131+ tests** (similar to T3) -- All packages: 100% test pass rate -- Performance benchmarks included - ---- - -## Success Criteria -βœ… All 8 T4 tasks complete -βœ… 131+ tests passing -βœ… Dashboard queryable in real-time -βœ… DAG visualization renders workflow dependencies -βœ… Cost tracking shows savings from T2 optimizations -βœ… Anomaly detection catches performance regressions -βœ… Multi-cluster deployment supported -βœ… Orchestrator can self-deploy - ---- - -## Timeline -- T4.1-T4.4: Week 1 (implementation + tests) -- T4.5-T4.8: Week 2 (implementation + tests) -- Integration testing: Week 3 -- Production deployment: Week 4 diff --git a/tasks/board.md b/tasks/board.md deleted file mode 100644 index dc405fb..0000000 --- a/tasks/board.md +++ /dev/null @@ -1,38 +0,0 @@ -# Task Board β€” Milestone T0 - -**Submilestone:** T0 (Multi-Agent Dev Orchestrator Temporal system) - -| ID | Scope | Status | Branch | Verification | Notes | -|----|-------|--------|--------|--------------|-------| -| T0.1 | Repo scaffold: go.mod, statemachine/, action/, cmd/, prompts/, internal/, tests/ | [x] | `task/T0.1` | `go build ./...` succeeds; layout matches PLAN.md | Foundation | -| T0.2 | Shared types: ModelSpec, PromptSpec, OrchestratorConfig, ActivityTuning, PiRetryPolicy | [x] | `task/T0.2` | Unit test asserts all defaults (5m/2s/30s/2.0/30s stream/2m stream-max) | Config data model | -| T0.3 | Git & locking: CloneRepoActivity, worktrees, squash-merge, orchestrator.lock | [x] | `task/T0.3` | Test vs local scratch repo: clone-if-empty vs fetch, worktree lifecycle, squash-merge produces 1 commit | Concurrency safety | -| T0.4 | PrepareSkillsActivity, classifyPiErr (4xx/5xx/504), stream timeout learning | [x] | `task/T0.4` | Unit tests: all 3 error buckets against mocked pi HTTP client | Pi integration | -| T0.5 | Planner/Judge/Implementer activities, LLM client, prompt templates | [x] | `task/T0.5` | Unit test: PromptSpec renders with system prompt + template override + raw template | LLM orchestration | -| T0.6 | TaskUnitWorkflow: retry loops (timeout/judge-fail split), lessons injection, escalation | [x] | `task/T0.6` | Implemented: retry loop, lessons injection, judge/implementer orchestration, timeout escalation | Task execution core | -| T0.7 | OrchestratorWorkflow: config state, signals, fan-out/fan-in, continue-as-new, 504 learning | [x] | `task/T0.7` | Implemented: planning cycle, fan-out/fan-in, 504 learning, continue-as-new, board updates | Orchestration core | -| T0.8 | cmd/worker, cmd/starter, internal/config (env/vsource loading) | [x] | `task/T0.8` | `go run ./cmd/worker` connects to temporal.riotpiao.com; `go run ./cmd/starter --dry-run` visible in Web UI | CLI integration | -| T0.9 | End-to-end: real temporal.riotpiao.com + disposable forgejo scratch repo, all 7 verification items | [x] | `task/T0.9` | Workflows implemented; fixture setup ready; E2E test successful against temporal.riotpiao.com | System validation complete | - -## Submission Criteria - -All T0.1–T0.9 marked `[x]` β†’ submilestone complete. - -At that point: -1. `git -C /workspace/Poimen/workflows checkout main && git pull` -2. `git merge --squash task/T0.1 task/T0.2 ... task/T0.9` -3. `git commit -m "T0: multi-agent orchestrator initial implementation"` -4. `git push origin main` -5. Delete all `task/T0.*` branches and worktrees - -This merge is the first real dogfood of the system's own git workflow: squashing 9 subtask branches into main as a single milestone commit. - ---- - -## Notes - -- **Lessons file location:** `tasks/.orchestrator/lessons/.jsonl` (created on first failure, not committed until Planner's board commit) -- **Branch naming:** Strict `task/T0.x` format; Orchestrator expects this pattern -- **Dry-run vs real:** T0.8 tests with `--dry-run` (no real push); T0.9 removes flag (real remote operations) -- **Temporal Web UI:** Monitor at `http://temporal.riotpiao.com:8080` (adjust port/host as needed) -- **E2E fixture:** Disposable forgejo repo (deleted post-run); confirm it's not a production repo before starting T0.9 diff --git a/tests/routing_e2e_test.go b/tests/routing_e2e_test.go new file mode 100644 index 0000000..f953f6a --- /dev/null +++ b/tests/routing_e2e_test.go @@ -0,0 +1,145 @@ +// +build integration + +package tests + +import ( + "context" + "encoding/json" + "os" + "testing" + "time" + + "github.com/rockliang/poimen/workflows/internal/routing" + "github.com/stretchr/testify/require" +) + +// TestRoutingE2E_GenerateAndValidate tests full flow against real api.riotpiao.com +// Run with: go test -tags=integration -v -run TestRoutingE2E ./tests/... +func TestRoutingE2E_GenerateAndValidate(t *testing.T) { + if os.Getenv("RUN_INTEGRATION_TESTS") != "1" { + t.Skip("Skipping integration test. Set RUN_INTEGRATION_TESTS=1 to run.") + } + + // Load knowledge base + kb, err := routing.LoadKnowledgeBaseFromDefaultPath() + require.NoError(t, err, "failed to load knowledge base") + + // Create router + router, err := routing.NewLLMRouter(kb) + require.NoError(t, err, "failed to create router") + + // Create validator + validator := routing.NewValidator(kb) + + tests := []struct { + name string + message string + context map[string]interface{} + isCron bool + }{ + { + name: "one-time repo analysis", + message: "Analyze https://github.com/rockliang/poimen for code quality and security issues", + context: map[string]interface{}{"branch": "main"}, + isCron: false, + }, + { + name: "scheduled security scan", + message: "Run daily security scan at 3 AM on https://github.com/rockliang/poimen", + isCron: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + // Generate workflow spec + input := routing.LLMRouterInput{ + Message: tt.message, + Context: tt.context, + } + + output, err := router.Route(ctx, input) + require.NoError(t, err, "LLM router failed") + + // Log generated spec + specJSON, _ := json.MarshalIndent(output, "", " ") + t.Logf("Generated spec:\n%s", string(specJSON)) + + // Validate based on type + if tt.isCron { + require.True(t, output.IsCron, "expected cron workflow") + require.NotNil(t, output.CronSpec, "cron spec is nil") + require.NotEmpty(t, output.CronSpec.Schedule, "cron schedule is empty") + + result := validator.ValidateCronWorkflowSpec(output.CronSpec) + require.True(t, result.Valid, "validation failed: %v", result.Errors) + + t.Logf("Cron workflow validated: %s (schedule: %s)", + output.CronSpec.Name, output.CronSpec.Schedule) + } else { + require.False(t, output.IsCron, "expected one-time workflow") + require.NotNil(t, output.Spec, "spec is nil") + + result := validator.ValidateWorkflowSpec(output.Spec) + require.True(t, result.Valid, "validation failed: %v", result.Errors) + + t.Logf("One-time workflow validated: %s (%d states)", + output.Spec.Name, len(output.Spec.States)) + } + }) + } +} + +// TestRoutingE2E_FullPipeline tests LLM router -> validation -> (simulated) execution +func TestRoutingE2E_FullPipeline(t *testing.T) { + if os.Getenv("RUN_INTEGRATION_TESTS") != "1" { + t.Skip("Skipping integration test. Set RUN_INTEGRATION_TESTS=1 to run.") + } + + kb, err := routing.LoadKnowledgeBaseFromDefaultPath() + require.NoError(t, err) + + router, err := routing.NewLLMRouter(kb) + require.NoError(t, err) + + validator := routing.NewValidator(kb) + + ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + defer cancel() + + // Generate workflow + output, err := router.Route(ctx, routing.LLMRouterInput{ + Message: "Clone and analyze https://github.com/rockliang/poimen for security vulnerabilities", + }) + require.NoError(t, err) + require.False(t, output.IsCron) + require.NotNil(t, output.Spec) + + // Validate + result := validator.ValidateWorkflowSpec(output.Spec) + require.True(t, result.Valid, "validation failed: %v", result.Errors) + + // Verify structure + require.NotEmpty(t, output.Spec.Name) + require.NotEmpty(t, output.Spec.States) + + // First state should be CloneRepoActivity + require.Equal(t, "CloneRepoActivity", output.Spec.States[0].Resource, + "expected first activity to be CloneRepoActivity") + + // Check that flaky activities have retry policies + for _, state := range output.Spec.States { + if state.Type == routing.StateTypeTask { + if kb.IsFlaky(state.Resource) { + require.NotNil(t, state.Retry, "flaky activity %s should have retry policy", state.Resource) + require.GreaterOrEqual(t, state.Retry.MaxAttempts, int32(2), + "flaky activity %s should have at least 2 retries", state.Resource) + } + } + } + + t.Logf("Full pipeline test passed: %s with %d states", output.Spec.Name, len(output.Spec.States)) +} diff --git a/tests/routing_workflow_test.go b/tests/routing_workflow_test.go new file mode 100644 index 0000000..d9b3f58 --- /dev/null +++ b/tests/routing_workflow_test.go @@ -0,0 +1,272 @@ +package tests + +import ( + "context" + "fmt" + "testing" + + "github.com/rockliang/poimen/workflows/internal/routing" + "github.com/rockliang/poimen/workflows/statemachine" + "github.com/stretchr/testify/require" + "go.temporal.io/sdk/testsuite" +) + +func TestRoutingWorkflow_SimpleWorkflow(t *testing.T) { + testSuite := &testsuite.WorkflowTestSuite{} + env := testSuite.NewTestWorkflowEnvironment() + + // Register mock activity + env.RegisterActivity(mockCloneRepoActivity) + + // Create simple workflow spec + spec := &routing.WorkflowSpec{ + Name: "test-workflow", + Input: map[string]interface{}{ + "repo": "https://github.com/test/repo", + "branch": "main", + }, + States: []routing.State{ + { + Name: "Clone", + Type: routing.StateTypeTask, + Resource: "mockCloneRepoActivity", + Parameters: map[string]interface{}{ + "repo": "${input.repo}", + "branch": "${input.branch}", + }, + Timeout: "5m", + Retry: &routing.RetryPolicy{ + MaxAttempts: 2, + BackoffRate: 1.5, + InitialInterval: "1s", + }, + End: true, + }, + }, + } + + input := statemachine.RoutingWorkflowInput{Spec: spec} + + env.ExecuteWorkflow(statemachine.RoutingWorkflow, input) + + require.True(t, env.IsWorkflowCompleted()) + require.NoError(t, env.GetWorkflowError()) + + var output statemachine.RoutingWorkflowOutput + require.NoError(t, env.GetWorkflowResult(&output)) + require.Equal(t, "COMPLETED", output.Status) + require.NotNil(t, output.FinalOutput) +} + +func TestRoutingWorkflow_MultiStepWorkflow(t *testing.T) { + testSuite := &testsuite.WorkflowTestSuite{} + env := testSuite.NewTestWorkflowEnvironment() + + // Register mock activities + env.RegisterActivity(mockCloneRepoActivity) + env.RegisterActivity(mockAnalyzeActivity) + + // Create multi-step workflow spec + spec := &routing.WorkflowSpec{ + Name: "multi-step-workflow", + Input: map[string]interface{}{ + "repo": "https://github.com/test/repo", + }, + States: []routing.State{ + { + Name: "Clone", + Type: routing.StateTypeTask, + Resource: "mockCloneRepoActivity", + Parameters: map[string]interface{}{ + "repo": "${input.repo}", + }, + Timeout: "5m", + Next: "Analyze", + }, + { + Name: "Analyze", + Type: routing.StateTypeTask, + Resource: "mockAnalyzeActivity", + Parameters: map[string]interface{}{ + "path": "${Clone.output.path}", + }, + Timeout: "10m", + End: true, + }, + }, + } + + input := statemachine.RoutingWorkflowInput{Spec: spec} + + env.ExecuteWorkflow(statemachine.RoutingWorkflow, input) + + require.True(t, env.IsWorkflowCompleted()) + require.NoError(t, env.GetWorkflowError()) + + var output statemachine.RoutingWorkflowOutput + require.NoError(t, env.GetWorkflowResult(&output)) + t.Logf("Output: %+v", output) + t.Logf("Error: %s", output.Error) + require.Equal(t, "COMPLETED", output.Status) + require.Contains(t, output.StepResults, "Clone") + require.Contains(t, output.StepResults, "Analyze") +} + +func TestRoutingWorkflow_PassState(t *testing.T) { + testSuite := &testsuite.WorkflowTestSuite{} + env := testSuite.NewTestWorkflowEnvironment() + + // Create workflow with Pass state + spec := &routing.WorkflowSpec{ + Name: "pass-state-workflow", + Input: map[string]interface{}{}, + States: []routing.State{ + { + Name: "StaticResult", + Type: routing.StateTypePass, + Result: map[string]interface{}{"status": "ok", "message": "static result"}, + End: true, + }, + }, + } + + input := statemachine.RoutingWorkflowInput{Spec: spec} + + env.ExecuteWorkflow(statemachine.RoutingWorkflow, input) + + require.True(t, env.IsWorkflowCompleted()) + require.NoError(t, env.GetWorkflowError()) + + var output statemachine.RoutingWorkflowOutput + require.NoError(t, env.GetWorkflowResult(&output)) + require.Equal(t, "COMPLETED", output.Status) +} + +func TestRoutingWorkflow_FailState(t *testing.T) { + testSuite := &testsuite.WorkflowTestSuite{} + env := testSuite.NewTestWorkflowEnvironment() + + // Create workflow with Fail state + spec := &routing.WorkflowSpec{ + Name: "fail-state-workflow", + Input: map[string]interface{}{}, + States: []routing.State{ + { + Name: "HandleError", + Type: routing.StateTypeFail, + Error: "WorkflowError", + Cause: "Something went wrong", + }, + }, + } + + input := statemachine.RoutingWorkflowInput{Spec: spec} + + env.ExecuteWorkflow(statemachine.RoutingWorkflow, input) + + require.True(t, env.IsWorkflowCompleted()) + require.NoError(t, env.GetWorkflowError()) + + var output statemachine.RoutingWorkflowOutput + require.NoError(t, env.GetWorkflowResult(&output)) + require.Equal(t, "FAILED", output.Status) + require.Contains(t, output.Error, "WorkflowError") +} + +func TestRoutingWorkflow_ErrorCatch(t *testing.T) { + testSuite := &testsuite.WorkflowTestSuite{} + env := testSuite.NewTestWorkflowEnvironment() + + // Register mock activities + env.RegisterActivity(mockFailingActivity) + + // Create workflow with error handling + spec := &routing.WorkflowSpec{ + Name: "error-catch-workflow", + Input: map[string]interface{}{}, + States: []routing.State{ + { + Name: "FlakyStep", + Type: routing.StateTypeTask, + Resource: "mockFailingActivity", + Parameters: map[string]interface{}{}, + Timeout: "1m", + Retry: &routing.RetryPolicy{ + MaxAttempts: 1, + BackoffRate: 1.0, + InitialInterval: "1s", + }, + Catch: []routing.CatchClause{ + { + ErrorEquals: []string{"ActivityError"}, + Next: "HandleError", + }, + }, + Next: "Success", + }, + { + Name: "Success", + Type: routing.StateTypePass, + Result: "success", + End: true, + }, + { + Name: "HandleError", + Type: routing.StateTypeFail, + Error: "CaughtError", + Cause: "Activity failed and was caught", + }, + }, + } + + input := statemachine.RoutingWorkflowInput{Spec: spec} + + env.ExecuteWorkflow(statemachine.RoutingWorkflow, input) + + require.True(t, env.IsWorkflowCompleted()) + require.NoError(t, env.GetWorkflowError()) + + var output statemachine.RoutingWorkflowOutput + require.NoError(t, env.GetWorkflowResult(&output)) + require.Equal(t, "FAILED", output.Status) + require.Contains(t, output.Error, "CaughtError") +} + +func TestRoutingWorkflow_EmptySpec(t *testing.T) { + testSuite := &testsuite.WorkflowTestSuite{} + env := testSuite.NewTestWorkflowEnvironment() + + // Empty spec + input := statemachine.RoutingWorkflowInput{Spec: nil} + + env.ExecuteWorkflow(statemachine.RoutingWorkflow, input) + + require.True(t, env.IsWorkflowCompleted()) + require.NoError(t, env.GetWorkflowError()) + + var output statemachine.RoutingWorkflowOutput + require.NoError(t, env.GetWorkflowResult(&output)) + require.Equal(t, "FAILED", output.Status) + require.Contains(t, output.Error, "empty") +} + +// Mock activities +func mockCloneRepoActivity(ctx context.Context, params map[string]interface{}) (map[string]interface{}, error) { + return map[string]interface{}{ + "path": "/tmp/cloned-repo", + "commit": "abc123", + "branch": "main", + }, nil +} + +func mockAnalyzeActivity(ctx context.Context, params map[string]interface{}) (map[string]interface{}, error) { + return map[string]interface{}{ + "quality": 0.85, + "issues": []string{}, + "summary": "Code analysis complete", + }, nil +} + +func mockFailingActivity(ctx context.Context, params map[string]interface{}) (map[string]interface{}, error) { + return nil, fmt.Errorf("mock activity failure") +} diff --git a/tests/temporal_routing_test.go b/tests/temporal_routing_test.go new file mode 100644 index 0000000..1e1d99d --- /dev/null +++ b/tests/temporal_routing_test.go @@ -0,0 +1,141 @@ +// +build integration + +package tests + +import ( + "context" + "os" + "testing" + "time" + + "github.com/rockliang/poimen/workflows/internal/routing" + "github.com/rockliang/poimen/workflows/statemachine" + "github.com/stretchr/testify/require" + "go.temporal.io/sdk/client" +) + +// TestTemporalRoutingWorkflow tests full flow against real Temporal cluster +// Run with: TEMPORAL_HOSTPORT=temporal.riotpiao.com:7233 RUN_INTEGRATION_TESTS=1 go test -tags=integration -v -run TestTemporalRoutingWorkflow ./tests/... +func TestTemporalRoutingWorkflow(t *testing.T) { + if os.Getenv("RUN_INTEGRATION_TESTS") != "1" { + t.Skip("Skipping integration test. Set RUN_INTEGRATION_TESTS=1 to run.") + } + + hostPort := os.Getenv("TEMPORAL_HOSTPORT") + if hostPort == "" { + hostPort = "temporal.riotpiao.com:7233" + } + + namespace := os.Getenv("TEMPORAL_NAMESPACE") + if namespace == "" { + namespace = "poimen-harness" + } + + t.Logf("Connecting to Temporal at %s (namespace: %s)", hostPort, namespace) + + // Connect to Temporal + c, err := client.Dial(client.Options{ + HostPort: hostPort, + Namespace: namespace, + }) + if err != nil { + t.Skipf("Skipping - cannot connect to Temporal: %v", err) + } + defer c.Close() + + t.Log("Connected to Temporal successfully") + + // Test 1: Generate spec via LLM and submit + t.Run("LLM_Route_And_Submit", func(t *testing.T) { + // Load KB and create router + kb, err := routing.LoadKnowledgeBaseFromDefaultPath() + require.NoError(t, err) + + router, err := routing.NewLLMRouter(kb) + require.NoError(t, err) + + // Generate workflow spec + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + output, err := router.Route(ctx, routing.LLMRouterInput{ + Message: "Clone and analyze https://github.com/rockliang/poimen", + }) + require.NoError(t, err) + require.False(t, output.IsCron) + require.NotNil(t, output.Spec) + + t.Logf("Generated spec: %s with %d states", output.Spec.Name, len(output.Spec.States)) + + // Submit to Temporal + workflowID := "test-routing-" + time.Now().Format("20060102-150405") + input := statemachine.RoutingWorkflowInput{Spec: output.Spec} + + run, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + ID: workflowID, + TaskQueue: "poimen-taskqueue", + }, statemachine.RoutingWorkflow, input) + require.NoError(t, err) + + t.Logf("Workflow submitted: ID=%s, RunID=%s", run.GetID(), run.GetRunID()) + + // Check workflow started (don't wait for completion - activities may not be registered) + desc, err := c.DescribeWorkflowExecution(ctx, workflowID, "") + require.NoError(t, err) + t.Logf("Workflow status: %s", desc.WorkflowExecutionInfo.Status.String()) + + // Cancel the workflow (since activities may not be running) + err = c.CancelWorkflow(ctx, workflowID, "") + if err != nil { + t.Logf("Cancel failed (may already be done): %v", err) + } else { + t.Log("Workflow cancelled") + } + }) + + // Test 2: Submit simple Pass-only workflow (no activities needed) + t.Run("PassOnly_Workflow", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + spec := &routing.WorkflowSpec{ + Name: "pass-only-test", + Input: map[string]interface{}{"test": true}, + States: []routing.State{ + { + Name: "Step1", + Type: routing.StateTypePass, + Result: map[string]interface{}{"status": "step1-done"}, + Next: "Step2", + }, + { + Name: "Step2", + Type: routing.StateTypePass, + Result: map[string]interface{}{"status": "step2-done", "final": true}, + End: true, + }, + }, + } + + workflowID := "test-pass-only-" + time.Now().Format("20060102-150405") + input := statemachine.RoutingWorkflowInput{Spec: spec} + + run, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + ID: workflowID, + TaskQueue: "poimen-taskqueue", + }, statemachine.RoutingWorkflow, input) + require.NoError(t, err) + + t.Logf("Pass-only workflow submitted: ID=%s", run.GetID()) + + // Wait for result (Pass states don't need workers) + var result statemachine.RoutingWorkflowOutput + err = run.Get(ctx, &result) + require.NoError(t, err) + + t.Logf("Workflow result: status=%s", result.Status) + require.Equal(t, "COMPLETED", result.Status) + require.Contains(t, result.StepResults, "Step1") + require.Contains(t, result.StepResults, "Step2") + }) +} diff --git a/worker b/worker index ca330ab..773ef53 100755 Binary files a/worker and b/worker differ