feat: RoutingWorkflow + LLM Router + Memory Activity
ci / test (push) Successful in 2m12s

- Add RoutingWorkflow: generic state machine executor for WorkflowSpec
- Add LLM Router: natural language → WorkflowSpec generation
- Add RetrieveMemoryActivity: query poimen-memory for context
- Add activities: AnalyzeCode, SecurityScan, GenerateReport, Notify, etc.
- Add agent-prompts/router: LLM prompt documentation
- Extend starter with --route flag for routing workflows
- Remove orchestrator job (trigger via API/message instead)
- Clean up: move docs to Desktop, add .gitignore for *.md
This commit is contained in:
Test
2026-09-02 19:21:53 -07:00
parent 5a465b145c
commit a0e64224a7
74 changed files with 3950 additions and 12421 deletions
+8
View File
@@ -0,0 +1,8 @@
# Ignore markdown docs except agent-prompts
*.md
!agent-prompts/*.md
!agent-prompts/**/*.md
# Binaries
starter
worker
poimen
-394
View File
@@ -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
-369
View File
@@ -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.** 🚀
-252
View File
@@ -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! 🎉
-888
View File
@@ -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.
-423
View File
@@ -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
File diff suppressed because it is too large Load Diff
-498
View File
@@ -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=<jwt-token-from-authentik>
```
### 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.
-683
View File
@@ -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<StepOutput> {
// 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.
-373
View File
@@ -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 <pod>",
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`
-337
View File
@@ -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>
<summary>Details</summary>
```
/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
```
</details>
---
### 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>
<summary>Details</summary>
- `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
</details>
---
### 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>
<summary>Details</summary>
**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
```
</details>
---
### 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>
<summary>Details</summary>
**`PrepareSkillsActivity`:**
- Input: `{Skills []SkillRef, StreamTimeout time.Duration}`
- For each skill, `pi clone-or-fetch <skill-url>` (idempotent)
- Each skill guarded by its own lock
**Error classification:**
```go
func classifyPiErr(err error) error {
// 4xx -> NonRetryableApplicationError "PiClientError"
// 504 -> ApplicationError "PiStreamTimeout"
// others -> retryable
}
```
</details>
---
### 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>
<summary>Details</summary>
**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`
</details>
---
### 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>
<summary>Details</summary>
**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.
</details>
---
### 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>
<summary>Details</summary>
**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
}
```
</details>
---
### 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>
<summary>Details</summary>
**`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
</details>
---
### 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>
<summary>Details</summary>
**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 [email protected]: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
</details>
---
## 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
-170
View File
@@ -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
-360
View File
@@ -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!
-327
View File
@@ -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
File diff suppressed because it is too large Load Diff
-269
View File
@@ -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`
-585
View File
@@ -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<bool>;
}
```
**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<Output>;
}
```
**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<u64>;
fn read(&self, range: Range<u64>) -> Result<Vec<WorkerEvent>>;
}
```
**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<Vec<u8>>;
}
```
**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.
+296
View File
@@ -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
}
+144
View File
@@ -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)
}
+49
View File
@@ -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)
}
}
+255
View File
@@ -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] + "..."
}
+138
View File
@@ -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)
}
+298
View File
@@ -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
}
+130
View File
@@ -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)
}
+89
View File
@@ -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
}
+59
View File
@@ -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`
+91
View File
@@ -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`
+143 -3
View File
@@ -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)
}
}
}
}
+20
View File
@@ -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)
+234
View File
@@ -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)
}
}
+122
View File
@@ -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
// }
+3
View File
@@ -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
+9
View File
@@ -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=
-531
View File
@@ -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 <pod>",
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`.
+69 -2
View File
@@ -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
}
}
}
+16
View File
@@ -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")
}
+110
View File
@@ -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
}
+456
View File
@@ -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.`
@@ -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)
}
})
}
}
+305
View File
@@ -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)
}
}
+2 -1
View File
@@ -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"
+2 -2
View File
@@ -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"
+2 -10
View File
@@ -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
-63
View File
@@ -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"
+11 -3
View File
@@ -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"
BIN
View File
Binary file not shown.
+228
View File
@@ -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
}
-137
View File
@@ -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 <skill-url>`
- 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<TaskName>
```
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: <how you verified it works>
```
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.1T0.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.
-125
View File
@@ -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/<TaskID>.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
-102
View File
@@ -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.1T0.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 T1T3 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).
-42
View File
@@ -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
-108
View File
@@ -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
-93
View File
@@ -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/<TaskID> ../worktrees/<id> 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
-72
View File
@@ -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
-133
View File
@@ -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
-78
View File
@@ -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/<TaskID>.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
-143
View File
@@ -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
-167
View File
@@ -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
-191
View File
@@ -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 <orch-id-from-run> \
--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 <orch-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 (`[email protected]: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
-263
View File
@@ -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
-223
View File
@@ -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.)
-371
View File
@@ -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.)
-443
View File
@@ -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
-434
View File
@@ -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
-174
View File
@@ -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
-19
View File
@@ -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.1T1.8 marked `[x]` → submilestone complete → squash-merge `task/T1.*` to main.
-19
View File
@@ -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.1T2.8 marked `[x]` → submilestone complete → squash-merge `task/T2.*` to main.
-19
View File
@@ -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.1T3.8 marked `[x]` → submilestone complete → squash-merge `task/T3.*` to main.
-128
View File
@@ -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
-38
View File
@@ -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.1T0.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/<TaskID>.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
+145
View File
@@ -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))
}
+272
View File
@@ -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")
}
+141
View File
@@ -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")
})
}
BIN
View File
Binary file not shown.