diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index d486add..b84a2ef 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2281,4 +2281,13 @@ target_link_libraries(step377_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step378_test tests/step378_test.cpp) +target_include_directories(step378_test PRIVATE src) +target_link_libraries(step378_test PRIVATE + nlohmann_json::nlohmann_json + unofficial::tree-sitter::tree-sitter + tree_sitter_python tree_sitter_cpp tree_sitter_elisp + tree_sitter_javascript tree_sitter_typescript + tree_sitter_java tree_sitter_rust tree_sitter_go) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/WorkflowOrchestrator.h b/editor/src/WorkflowOrchestrator.h new file mode 100644 index 0000000..49d7d2e --- /dev/null +++ b/editor/src/WorkflowOrchestrator.h @@ -0,0 +1,231 @@ +#pragma once + +#include "WorkflowState.h" +#include "RoutingEngine.h" +#include "WorkerRegistry.h" +#include "ContextAssembler.h" +#include "ReviewGate.h" +#include +#include +#include + +struct OrchestratorEvent { + std::string type; + std::string itemId; + json detail; + std::string timestamp; +}; + +struct BlockerInfo { + std::string type; + std::vector itemIds; + std::string description; +}; + +class WorkflowOrchestrator { +public: + WorkflowOrchestrator(WorkflowState& workflowState, + RoutingEngine& routingEngine, + WorkerRegistry& workerRegistry, + ContextAssembler& contextAssembler, + ReviewGate& reviewGate) + : state_(&workflowState), routing_(&routingEngine), workers_(&workerRegistry), + context_(&contextAssembler), review_(&reviewGate) {} + + void setBuffers(const std::map& buffers) { + buffers_ = buffers; + } + + void setReviewPolicy(const ReviewPolicy& policy) { + reviewPolicy_ = policy; + } + + OrchestratorEvent step() { + auto ready = state_->queue.getReady(); + if (ready.empty()) { + return blockedEvent(); + } + return advanceItem(ready.front()).back(); + } + + std::vector advance() { + std::vector all; + auto ready = state_->queue.getReady(); + for (const auto& item : ready) { + auto events = advanceItem(item); + all.insert(all.end(), events.begin(), events.end()); + } + return all; + } + + WorkflowStats runToCompletion() { + while (true) { + auto events = advance(); + if (events.empty()) break; + bool anyProgress = false; + for (const auto& ev : events) { + if (ev.type != "blocked") { + anyProgress = true; + break; + } + } + if (!anyProgress) break; + } + return state_->getStats(); + } + + WorkflowStats runUntil(const std::function& predicate) { + while (!predicate(*state_)) { + auto events = advance(); + if (events.empty()) break; + bool anyProgress = false; + for (const auto& ev : events) { + if (ev.type != "blocked") { + anyProgress = true; + break; + } + } + if (!anyProgress) break; + } + return state_->getStats(); + } + + std::vector getBlockers() { + std::vector out; + + auto reviewItems = state_->queue.getByStatus(WI_REVIEW); + if (!reviewItems.empty()) { + BlockerInfo b; + b.type = "needs-review"; + b.description = "Items are waiting in review."; + for (const auto& i : reviewItems) b.itemIds.push_back(i.id); + out.push_back(b); + } + + BlockerInfo human; + human.type = "needs-human"; + human.description = "Items are routed to human execution."; + + BlockerInfo external; + external.type = "needs-external-model"; + external.description = "Items are prepared for SLM/LLM external execution."; + + for (const auto& status : {WI_ASSIGNED, WI_IN_PROGRESS}) { + for (const auto& item : state_->queue.getByStatus(status)) { + if (item.workerType == "human") { + human.itemIds.push_back(item.id); + } else if (item.workerType == "slm" || item.workerType == "llm") { + external.itemIds.push_back(item.id); + } + } + } + + if (!human.itemIds.empty()) out.push_back(human); + if (!external.itemIds.empty()) out.push_back(external); + return out; + } + +private: + WorkflowState* state_ = nullptr; + RoutingEngine* routing_ = nullptr; + WorkerRegistry* workers_ = nullptr; + ContextAssembler* context_ = nullptr; + ReviewGate* review_ = nullptr; + ReviewPolicy reviewPolicy_ = ReviewPolicy::getDefault(); + std::map buffers_; + + static OrchestratorEvent makeEvent(const std::string& type, + const std::string& itemId, + const json& detail = json::object()) { + OrchestratorEvent ev; + ev.type = type; + ev.itemId = itemId; + ev.detail = detail; + ev.timestamp = workItemTimestamp(); + return ev; + } + + OrchestratorEvent blockedEvent() { + auto blockers = getBlockers(); + if (blockers.empty()) { + return makeEvent("blocked", "", json{{"reason", "no-ready-items"}}); + } + return makeEvent("blocked", "", json{{"blockers", toJsonBlockers(blockers)}}); + } + + static json toJsonBlockers(const std::vector& blockers) { + json arr = json::array(); + for (const auto& b : blockers) { + arr.push_back({{"type", b.type}, + {"itemIds", b.itemIds}, + {"description", b.description}}); + } + return arr; + } + + std::vector advanceItem(const WorkItem& readyItem) { + std::vector events; + + auto maybeItem = state_->queue.getItem(readyItem.id); + if (!maybeItem.has_value()) return events; + WorkItem item = maybeItem.value(); + + RoutingDecision decision = routing_->route(item); + item.workerType = decision.workerType; + item.contextWidth = decision.contextWidth; + item.reviewRequired = decision.reviewRequired; + + transitionWorkItem(item, WI_ASSIGNED); + transitionWorkItem(item, WI_IN_PROGRESS); + state_->queue.updateItem(item.id, item); + + events.push_back(makeEvent("routed", item.id, decision.toJson())); + + WorkerContext ctx = context_->assembleContext(item, buffers_, item.contextWidth, + decision.contextBudgetTokens); + events.push_back(makeEvent("context-assembled", item.id, + json{{"contextWidth", item.contextWidth}, + {"tokensBudget", decision.contextBudgetTokens}})); + + WorkerInterface* worker = workers_->getWorker(item.workerType); + if (!worker) { + events.push_back(makeEvent("blocked", item.id, + json{{"reason", "worker-not-registered"}, + {"workerType", item.workerType}})); + return events; + } + + item.result = worker->execute(item, ctx); + state_->queue.updateItem(item.id, item); + events.push_back(makeEvent("executed", item.id, + json{{"workerType", item.workerType}, + {"confidence", item.result.confidence}})); + + if (item.workerType == "human") { + events.push_back(makeEvent("blocked", item.id, + json{{"reason", "needs-human"}})); + return events; + } + if (item.workerType == "slm" || item.workerType == "llm") { + events.push_back(makeEvent("blocked", item.id, + json{{"reason", "needs-external-model"}})); + return events; + } + + ReviewDecision reviewDecision = review_->shouldAutoApprove(item, item.result, reviewPolicy_); + if (reviewDecision.approved) { + state_->queue.updateItem(item.id, item); + state_->queue.complete(item.id); + events.push_back(makeEvent("auto-approved", item.id, + json{{"rule", reviewDecision.ruleMatched}})); + events.push_back(makeEvent("completed", item.id)); + } else { + transitionWorkItem(item, WI_REVIEW); + state_->queue.updateItem(item.id, item); + events.push_back(makeEvent("sent-to-review", item.id, + json{{"reason", reviewDecision.reasoning}})); + } + + return events; + } +}; diff --git a/editor/tests/step378_test.cpp b/editor/tests/step378_test.cpp new file mode 100644 index 0000000..70329e8 --- /dev/null +++ b/editor/tests/step378_test.cpp @@ -0,0 +1,249 @@ +// Step 378: WorkflowOrchestrator main loop (12 tests) + +#include +#include +#include +#include "WorkflowOrchestrator.h" + +static WorkItem makeItem(const std::string& id, + const std::string& name, + const std::string& workerType, + bool reviewRequired = false) { + WorkItem w; + w.id = id; + w.nodeId = id + "_node"; + w.nodeName = name; + w.nodeType = "Function"; + w.bufferId = "buf"; + w.contextWidth = "local"; + w.workerType = workerType; + w.reviewRequired = reviewRequired; + w.priority = "medium"; + w.status = WI_PENDING; + w.createdAt = workItemTimestamp(); + return w; +} + +int main() { + int passed = 0; + + // Test 1: single deterministic/template task completes in one step + { + WorkflowState ws("p1"); + ws.queue.enqueue(makeItem("wi1", "getName", "template")); + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + + OrchestratorEvent ev = orch.step(); + assert(ev.type == "completed"); + assert(ws.queue.getByStatus(WI_COMPLETE).size() == 1); + std::cout << "Test 1 PASSED: single template task completes in one step\n"; + passed++; + } + + // Test 2: dependency cascade after completion + { + WorkflowState ws("p2"); + WorkItem a = makeItem("a", "getA", "template"); + WorkItem b = makeItem("b", "getB", "template"); + b.dependencies.push_back("a"); + ws.queue.enqueue(a); + ws.queue.enqueue(b); + + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + orch.step(); + assert(ws.queue.getByStatus(WI_COMPLETE).size() == 1); + assert(ws.queue.getByStatus(WI_READY).size() == 1); // b promoted + std::cout << "Test 2 PASSED: dependency cascade promotion\n"; + passed++; + } + + // Test 3: human task blocks as needs-human + { + WorkflowState ws("p3"); + ws.queue.enqueue(makeItem("wi3", "hardTask", "human")); + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + orch.step(); + auto blockers = orch.getBlockers(); + bool hasHuman = false; + for (const auto& b : blockers) if (b.type == "needs-human") hasHuman = true; + assert(hasHuman); + std::cout << "Test 3 PASSED: human task blocks\n"; + passed++; + } + + // Test 4: SLM task blocks as needs-external-model + { + WorkflowState ws("p4"); + ws.queue.enqueue(makeItem("wi4", "fileTask", "slm")); + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + orch.step(); + auto blockers = orch.getBlockers(); + bool hasExternal = false; + for (const auto& b : blockers) if (b.type == "needs-external-model") hasExternal = true; + assert(hasExternal); + std::cout << "Test 4 PASSED: agent task blocks for external model\n"; + passed++; + } + + // Test 5: runToCompletion processes deterministic tasks + { + WorkflowState ws("p5"); + ws.queue.enqueue(makeItem("w1", "getOne", "template")); + ws.queue.enqueue(makeItem("w2", "getTwo", "template")); + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + auto stats = orch.runToCompletion(); + assert(stats.complete == 2); + std::cout << "Test 5 PASSED: runToCompletion completes deterministic tasks\n"; + passed++; + } + + // Test 6: advance emits stage events + { + WorkflowState ws("p6"); + ws.queue.enqueue(makeItem("wi6", "getX", "template")); + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + auto events = orch.advance(); + bool routed = false, context = false, executed = false, completed = false; + for (const auto& e : events) { + if (e.type == "routed") routed = true; + if (e.type == "context-assembled") context = true; + if (e.type == "executed") executed = true; + if (e.type == "completed") completed = true; + } + assert(routed && context && executed && completed); + std::cout << "Test 6 PASSED: stage events emitted\n"; + passed++; + } + + // Test 7: blockers report review items + { + WorkflowState ws("p7"); + ws.queue.enqueue(makeItem("wi7", "getY", "template", true)); // force review + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + orch.step(); + auto blockers = orch.getBlockers(); + bool hasReview = false; + for (const auto& b : blockers) if (b.type == "needs-review") hasReview = true; + assert(hasReview); + std::cout << "Test 7 PASSED: review blockers reported\n"; + passed++; + } + + // Test 8: empty workflow returns immediately + { + WorkflowState ws("p8"); + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + auto stats = orch.runToCompletion(); + assert(stats.total == 0); + auto ev = orch.step(); + assert(ev.type == "blocked"); + std::cout << "Test 8 PASSED: empty workflow no-op\n"; + passed++; + } + + // Test 9: mixed workflow advances what it can + { + WorkflowState ws("p9"); + ws.queue.enqueue(makeItem("d1", "getFast", "template")); + ws.queue.enqueue(makeItem("h1", "manual", "human")); + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + auto stats = orch.runToCompletion(); + assert(stats.complete == 1); + assert(!orch.getBlockers().empty()); + std::cout << "Test 9 PASSED: mixed workflow partially advances\n"; + passed++; + } + + // Test 10: runUntil predicate can stop early + { + WorkflowState ws("p10"); + ws.queue.enqueue(makeItem("a1", "getA", "template")); + ws.queue.enqueue(makeItem("a2", "getB", "template")); + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + auto stats = orch.runUntil([](const WorkflowState& s) { + return s.getStats().complete >= 1; + }); + assert(stats.complete >= 1); + std::cout << "Test 10 PASSED: runUntil predicate stop\n"; + passed++; + } + + // Test 11: blocked event includes blocker detail when applicable + { + WorkflowState ws("p11"); + ws.queue.enqueue(makeItem("h2", "manual2", "human")); + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + orch.step(); // process human task first + OrchestratorEvent ev = orch.step(); // now no ready, returns blocked + assert(ev.type == "blocked"); + assert(ev.detail.contains("blockers")); + std::cout << "Test 11 PASSED: blocked event includes blocker detail\n"; + passed++; + } + + // Test 12: agent task stores context payload in result + { + WorkflowState ws("p12"); + ws.queue.enqueue(makeItem("ll1", "llTask", "llm")); + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + orch.step(); + auto item = ws.queue.getItem("ll1"); + assert(item.has_value()); + assert(item->result.astJson.is_object()); + assert(item->result.astJson.contains("workerType")); + std::cout << "Test 12 PASSED: agent context payload preserved\n"; + passed++; + } + + std::cout << "\nResults: " << passed << "/12\n"; + assert(passed == 12); + return 0; +} diff --git a/progress.md b/progress.md index bd4379c..ae2e02a 100644 --- a/progress.md +++ b/progress.md @@ -2744,6 +2744,51 @@ Scheme. - `editor/tests/step377_test.cpp` remains within project file-size guidance for test artifacts (`188` lines) +### Step 378: Orchestrator Core Main Loop +**Status:** PASS (12/12 tests) + +Implemented Sprint 15 orchestration-loop core as a dedicated workflow engine +that advances ready work items through routing, context assembly, execution, +review, completion, and blocker reporting. + +**Files created:** +- `editor/src/WorkflowOrchestrator.h` — orchestration core: + - `step()` (advance one ready item one stage-set) + - `advance()` (advance all currently ready items) + - `runToCompletion()` (progress until no further non-blocked progress) + - `runUntil(predicate)` (predicate-controlled progression) + - `getBlockers()` with typed blockers: + `needs-human`, `needs-review`, `needs-external-model` + - event stream via `OrchestratorEvent` (`routed`, `context-assembled`, + `executed`, `auto-approved`, `sent-to-review`, `completed`, `blocked`) +- `editor/tests/step378_test.cpp` — 12 tests covering: + 1. single deterministic/template completion in one step + 2. dependency cascade promotion + 3. human-task blocker classification + 4. agent-task external-model blocker classification + 5. `runToCompletion()` deterministic completion behavior + 6. stage event emission coverage + 7. review blocker reporting + 8. empty-workflow no-op behavior + 9. mixed workflow partial progress behavior + 10. `runUntil(...)` predicate stop behavior + 11. blocked-event detail payload includes blockers + 12. agent context payload preservation + +**Files modified:** +- `editor/CMakeLists.txt` — `step378_test` target + +**Verification run:** +- `step378_test` — PASS (12/12) new step coverage +- `step377_test` — PASS (8/8) regression coverage +- `step376_test` — PASS (12/12) regression coverage +- `step375_test` — PASS (12/12) regression coverage +- `step331_test` — PASS (8/8) orchestration-foundation regression + +**Architecture gate check:** +- `editor/src/WorkflowOrchestrator.h` is within header size limit + (`231` lines <= `600`) + # Roadmap Planning — Sprints 12-25+ ## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)