diff --git a/CHANGELOG.md b/CHANGELOG.md index 4077ee6f..4d20112f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ### Removed +- **`langgraph_engine/helper_nodes/` is deleted** (5 modules, 538 lines). All nine of its public node functions were also defined in `orchestrator.py`, which is the module the graph actually wires, and nothing imported the package -- it was an extraction that was started and never completed. Two documentation corrections existed **only** in the orphaned copy and were ported to `orchestrator.py` before the deletion rather than discarded with it: `save_workflow_memory`'s comment named `~/.claude/memory/sessions/` while its code writes to `memory/logs/sessions/`, and `optimize_context_after_level1` still said "Level 3" in three places, a level that stopped existing in the `06b0463` rename. The live module had the stale text in both cases. + - **Step 1 no longer decomposes or executes the prompt it emits** (fb7bbaa). Phase 2 ran `todo_decomposer` to split the orchestration prompt into a TODO list and `orchestrator_agent_caller` once per TODO, each a nested `claude -p` subprocess. Both re-derived work that belongs elsewhere: the master template's STEP 13 already produces the MULTI-AGENT PROMPT BUNDLE, and the node runs inside a hook subprocess, so it cannot execute in the session it is serving -- it can only hand that session something to run. `orchestrator_result` now records what was emitted (`mode`, `prompt_chars`, `template_source`, `library_version`) rather than what was executed. An empty dict would have satisfied the contracts equally well, but `flow_trace_converter` and `decision_explainer` both read the field to build reports, and handing them a hollow value to keep a postcondition green is how a trace starts lying about what happened. - **The three Step 1 Phase 2 scripts are deleted** (c1d1d47): `todo_decomposer.py`, `todo_executor.py`, `orchestrator_agent_caller.py`, together with `tests/test_todo_decomposition.py` (27 tests, which imported them directly and covered nothing else). They had no caller after fb7bbaa. Dead code that still looks live is worse than absent code -- the next reader would have found three plausible scripts describing an execution model the pipeline no longer has. - **Four FlowState fields dropped** (e59a271): `todo_list`, `todo_results`, `completed_todos`, `current_todo_index`. With the scripts gone they had neither a writer nor a reader anywhere in the engine. `completed_todos` additionally declared an `_merge_lists` parallel-write reducer for a value no node produces. diff --git a/CLAUDE.md b/CLAUDE.md index 23b5a429..37a1b00b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,7 +20,7 @@ Claude Workflow Engine is a 3-level LangGraph-based orchestration pipeline for a | **Status** | Active Development | | **Primary Location** | langgraph_engine/ | | **MCP Servers** | 13 servers -- all in separate repos under [techdeveloper-org](https://github.com/orgs/techdeveloper-org/repositories); 1 also keeps an in-engine copy in `src/mcp/` | -| **Total Python Files** | 253 (langgraph_engine/); 493 repo-wide | +| **Total Python Files** | 248 (langgraph_engine/); 488 repo-wide | | **Test Files** | 100 (91 unit, 5 integration, 3 e2e, 1 load) | | **Call Graph** | 578 classes, 3,985 methods, 4 languages (Python/Java/TS/Kotlin) | @@ -141,7 +141,7 @@ Level 2: SDLC Execution Core (9 active steps: Steps 0-8) | +-- project_session.py # Session utilities (imported by hook packages) | +-- policy_tracking_helper.py # Policy tracking (imported by hook packages) +-- langgraph_engine/ # Core orchestration engine (REPO ROOT -- sibling of scripts/, NOT nested under it) -| +-- core/ state/ routing/ helper_nodes/ # LazyLoader/ErrorHandler, FlowState, routing, helper nodes +| +-- core/ state/ routing/ # LazyLoader/ErrorHandler, FlowState, routing | +-- diagrams/ (+drawio/) parsers/ integrations/ # Strategy/Factory: UML gens, 4 lang parsers, GitHub/Jira/Figma/Jenkins | +-- analysis/ context/ engine_logging/ github/ metrics/ quality/ security/ skills/ standards/ # domain subpackages (v1.20 migration) | +-- build_dependency_resolver/ runtime_verification/ # build-dep parsers; node contracts + verifier @@ -177,7 +177,6 @@ Level 2: SDLC Execution Core (9 active steps: Steps 0-8) | State Package | langgraph_engine/state/ | FlowState, StepKeys, reducers, WorkflowContextOptimizer | | Core Package | langgraph_engine/core/ | LazyLoader, get_logger, node_error_handler, NodeResult, create_step_node | | Routing Package | langgraph_engine/routing/ | All routing functions split by level | -| Helper Nodes | langgraph_engine/helper_nodes/ | Orphaned package -- no live importers anywhere in the repo (confirmed by repo-wide grep); kept internally consistent but scheduled for removal in a follow-up cleanup | | Graph Factory | langgraph_engine/orchestrator.py | create_flow_graph(hook_mode): single canonical StateGraph factory (verify_node runtime-verification wrapping applied here) | | Diagrams Package | langgraph_engine/diagrams/ | Strategy Pattern: DiagramFactory + 13 generators | | Parsers Package | langgraph_engine/parsers/ | Abstract Factory: ParserRegistry + 4 language parsers | diff --git a/langgraph_engine/helper_nodes/__init__.py b/langgraph_engine/helper_nodes/__init__.py deleted file mode 100644 index 3f7e6916..00000000 --- a/langgraph_engine/helper_nodes/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Helper nodes module - All non-subgraph node functions for the pipeline graph. - -Extracted from orchestrator.py to separate helper logic from graph construction. -Organized by concern: preflight_guard_helpers, context_helpers, output_helpers, -step_helpers, standards_helpers. -""" - -from .context_helpers import optimize_context_after_level1, optimize_context_for_level3_step, save_workflow_memory -from .output_helpers import output_node, synthesize_prompt_with_flow_data, verify_prompt_integrity -from .step_helpers import step5_retry_increment_node - -__all__ = [ - "optimize_context_after_level1", - "optimize_context_for_level3_step", - "save_workflow_memory", - "verify_prompt_integrity", - "synthesize_prompt_with_flow_data", - "output_node", - "step5_retry_increment_node", -] diff --git a/langgraph_engine/helper_nodes/context_helpers.py b/langgraph_engine/helper_nodes/context_helpers.py deleted file mode 100644 index caef55a2..00000000 --- a/langgraph_engine/helper_nodes/context_helpers.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Context helper nodes - Context optimization and workflow memory persistence. - -Extracted from orchestrator.py. Handles context compression between levels -and saving workflow memory for session persistence. -""" - -from pathlib import Path - -try: - import sys as _sys - - _sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent / "src")) - from utils.path_resolver import get_claude_home - - _CONTEXT_HELPERS_CLAUDE_HOME = get_claude_home() -except ImportError: - _CONTEXT_HELPERS_CLAUDE_HOME = Path.home() / ".claude" - -from ..core.logger_factory import get_logger -from ..flow_state import FlowState, StepKeys, WorkflowContextOptimizer - -logger = get_logger(__name__) - - -def optimize_context_after_level1(state: FlowState) -> dict: - """Optimize context after Level 1 completes before passing to Level 2. - - Stores full Level 1 output in workflow_memory, passes only summary to Level 2. - This keeps context clean while preserving full data for fallback/debugging. - """ - # Store full level 1 output - level1_output = { - StepKeys.CONTEXT_LOADED: state.get(StepKeys.CONTEXT_LOADED), - StepKeys.CONTEXT_PERCENTAGE: state.get(StepKeys.CONTEXT_PERCENTAGE), - StepKeys.SESSION_CHAIN_LOADED: state.get(StepKeys.SESSION_CHAIN_LOADED), - StepKeys.PATTERNS_DETECTED: state.get(StepKeys.PATTERNS_DETECTED, []), - StepKeys.PREFERENCES_DATA: state.get(StepKeys.PREFERENCES_DATA, {}), - } - - state = WorkflowContextOptimizer.store_step_output(state, "level1_output", level1_output) - - # Build optimized context for Level 2 - optimized = WorkflowContextOptimizer.build_optimized_context(state) - state["workflow_context_optimized"] = optimized - - return state - - -def optimize_context_for_level3_step(state: FlowState, step_name: str) -> dict: - """Optimize context before each Level 3 step. - - For Level 3, only pass data specific to current step. - All previous step outputs stay in workflow_memory. - """ - # Store current step's inputs/outputs - current_data = {k: v for k, v in state.items() if k.startswith("step") and isinstance(v, dict)} - - state = WorkflowContextOptimizer.store_step_output(state, step_name, current_data) - - return state - - -def save_workflow_memory(state: FlowState) -> dict: - """Save workflow memory to disk for session persistence.""" - try: - import json - - session_id = state.get(StepKeys.SESSION_ID, "unknown") - memory = state.get(StepKeys.WORKFLOW_MEMORY, {}) - - if memory and session_id != "unknown": - # Save to ~/.claude/memory/logs/sessions/{session_id}/workflow-memory.json - memory_dir = _CONTEXT_HELPERS_CLAUDE_HOME / "memory" / "logs" / "sessions" / session_id - memory_dir.mkdir(parents=True, exist_ok=True) - - memory_file = memory_dir / "workflow-memory.json" - with open(memory_file, "w", encoding="utf-8") as f: - json.dump( - { - "session_id": session_id, - "timestamp": state.get(StepKeys.TIMESTAMP), - "workflow_memory": memory, - "context_optimization_stats": state.get(StepKeys.STEP_OPTIMIZATION_STATS, {}), - "memory_size_kb": state.get(StepKeys.WORKFLOW_MEMORY_SIZE_KB, 0), - }, - f, - indent=2, - ) - - return {"workflow_memory_file": str(memory_file)} - except (OSError, TypeError) as exc: - logger.debug(f"[context_helpers] workflow memory save skipped: {exc}") - - return {} diff --git a/langgraph_engine/helper_nodes/output_helpers.py b/langgraph_engine/helper_nodes/output_helpers.py deleted file mode 100644 index a45f78ef..00000000 --- a/langgraph_engine/helper_nodes/output_helpers.py +++ /dev/null @@ -1,382 +0,0 @@ -"""Output helper nodes - Final output synthesis and pipeline execution logging. - -Extracted from orchestrator.py. Handles the final output_node which synthesizes -the prompt, saves the execution log, and accumulates session info. - -CHANGE LOG (v1.15.0): - Removed TOON log line from _save_pipeline_execution_log() Level 1 section - (TOON compression node removed from pipeline). - -CHANGE LOG (v1.15.2): - Removed step_info rows for steps 4-7 from _save_pipeline_execution_log() - (TOON Refinement, Skill & Agent Selection, Skill Validation, Final Prompt - Generation were removed from the pipeline in v1.13.0). -""" - -import sys -from pathlib import Path - -from ..core.logger_factory import get_logger - -try: - import sys as _sys - - _sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent / "src")) - from utils.path_resolver import get_claude_home - - _OUTPUT_HELPERS_CLAUDE_HOME = get_claude_home() -except ImportError: - _OUTPUT_HELPERS_CLAUDE_HOME = Path.home() / ".claude" - -from ..flow_state import FlowState, StepKeys -from .context_helpers import save_workflow_memory - -logger = get_logger(__name__) - - -def verify_prompt_integrity(state: FlowState) -> bool: - """Verify that original user prompt was never modified during flow. - - CRITICAL: user_message must equal user_message_original - If modified, that's a bug in the flow design. - """ - original = state.get(StepKeys.USER_MESSAGE_ORIGINAL, "") - current = state.get(StepKeys.USER_MESSAGE, "") - - if original != current: - print("[WARNING] PROMPT INTEGRITY VIOLATION!", file=sys.stderr) - print(f" Original: {original[:50]}...", file=sys.stderr) - print(f" Current: {current[:50]}...", file=sys.stderr) - return False - - return True - - -def synthesize_prompt_with_flow_data(state: FlowState) -> dict: - """Synthesize comprehensive prompt using all collected 3-level flow data. - - SYNTHESIS PROCESS: - 1. Collect data from all levels - 2. Call PromptGenerator.synthesize_with_flow_data() - 3. Create comprehensive prompt - 4. Return synthesized prompt for actual work - """ - try: - import importlib.util - - # Import PromptGenerator from prompt-generation-policy.py - scripts_path = ( - Path(__file__).parent.parent.parent - / "scripts" - / "architecture" - / "03-execution-system" - / "00-prompt-generation" - ) - spec = importlib.util.spec_from_file_location( - "prompt_generation_policy", scripts_path / "prompt-generation-policy.py" - ) - pg_module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(pg_module) - - generator = pg_module.PromptGenerator() - - # Prepare flow data for synthesis - use ACTUAL pipeline results - # Collect validated tasks for TodoList - validated_tasks = state.get(StepKeys.TASKS_VALIDATED, []) - raw_tasks = state.get(StepKeys.TASKS, {}).get("tasks", []) - all_tasks = validated_tasks if validated_tasks else raw_tasks - - # Collect plan phases - plan_exec = state.get(StepKeys.PLAN_EXECUTION, {}) - plan_phases = plan_exec.get("phases", []) if isinstance(plan_exec, dict) else [] - - flow_data = { - "level0_preflight": { - "unicode_check": state.get(StepKeys.UNICODE_CHECK, False), - "encoding_check": state.get(StepKeys.ENCODING_CHECK, False), - "windows_path_check": state.get(StepKeys.WINDOWS_PATH_CHECK, False), - }, - "level1_context_sync": { - "context_percentage": state.get(StepKeys.CONTEXT_PERCENTAGE, 0), - "session_chain_loaded": state.get(StepKeys.SESSION_CHAIN_LOADED, False), - "patterns_detected": state.get(StepKeys.PATTERNS_DETECTED, []), - "project_type": state.get(StepKeys.DETECTED_FRAMEWORK, "Unknown"), - }, - "standards": { - "standards_count": state.get(StepKeys.STANDARDS_COUNT, 0), - "is_java_project": state.get(StepKeys.IS_JAVA_PROJECT, False), - "java_standards_loaded": state.get(StepKeys.JAVA_STANDARDS_LOADED, False), - }, - "level2_sdlc_pipeline": { - "task_type": state.get(StepKeys.TASK_TYPE, "General"), - "complexity": state.get(StepKeys.COMPLEXITY, 5), - "suggested_model": state.get(StepKeys.SELECTED_MODEL, "complex_reasoning"), - "plan_mode_suggested": state.get(StepKeys.PLAN_REQUIRED, False), - "reasoning": state.get(StepKeys.REASONING, ""), - "tasks": all_tasks, - "task_count": len(all_tasks), - "plan_phases": plan_phases, - "selected_skill": state.get(StepKeys.SKILL, ""), - "selected_agent": state.get(StepKeys.AGENT, ""), - "selected_skills": state.get(StepKeys.SKILLS, []), - "selected_agents": state.get(StepKeys.AGENTS, []), - "skill_definition": state.get(StepKeys.SKILL_DEFINITION, ""), - "agent_definition": state.get(StepKeys.AGENT_DEFINITION, ""), - }, - } - - # SYNTHESIS: Create comprehensive prompt - # Priority: user_message > user_message_original > env var CURRENT_USER_MESSAGE - import os - - user_msg = ( - state.get(StepKeys.USER_MESSAGE) - or state.get(StepKeys.USER_MESSAGE_ORIGINAL) - or os.environ.get("CURRENT_USER_MESSAGE", "") - ) - synthesis_result = generator.synthesize_with_flow_data(user_msg, flow_data) - - return { - "synthesized_prompt": synthesis_result.get("synthesized_prompt", ""), - "synthesis_metadata": { - "original_message": synthesis_result.get("original_message"), - "context_level": synthesis_result.get("context_level"), - "data_used": synthesis_result.get("data_used"), - }, - } - - except Exception as e: - print(f"[WARNING] Prompt synthesis failed: {e}", file=sys.stderr) - # Fallback: return original message - return { - "synthesized_prompt": state.get(StepKeys.USER_MESSAGE, ""), - "synthesis_metadata": {"status": "fallback"}, - } - - -def output_node(state: FlowState) -> dict: - """Final output node - determines completion status and synthesizes final prompt.""" - # Verify prompt integrity before finishing - if not verify_prompt_integrity(state): - # Log but don't block - prompt should never be modified - pass - - # Write session-start voice flag so Stop hook speaks a greeting - # Use LEGACY flag (no PID) because pipeline runs in a separate process - # from Claude Code. Stop-notifier's _resolve_flag checks PID first, then legacy. - try: - flag_dir = _OUTPUT_HELPERS_CLAUDE_HOME - start_flag = flag_dir / ".session-start-voice" - if not start_flag.exists(): - task_type = state.get(StepKeys.TASK_TYPE, "task") - complexity = state.get(StepKeys.COMPLEXITY, 5) - skill = state.get(StepKeys.SKILL, "") - msg = f"Starting {task_type} task, complexity {complexity} out of 10." - if skill: - msg += f" Using {skill} skill." - start_flag.write_text(msg, encoding="utf-8") - except OSError as exc: - logger.debug(f"[output] voice start-flag write skipped: {exc}") - - # SYNTHESIS: Create comprehensive prompt from all flow data - synthesis = synthesize_prompt_with_flow_data(state) - - # Save workflow memory before finishing - save_workflow_memory(state) - - # Determine final status based on actual step results (not just errors list) - if state.get(StepKeys.PREFLIGHT_STATUS) == "BLOCKED": - final_status = "BLOCKED" - else: - step_failures = [] - for key, val in state.items(): - if key.endswith("_status") and isinstance(val, str) and val == "ERROR": - step_failures.append(key) - if step_failures: - final_status = "FAILED" - elif state.get(StepKeys.WARNINGS): - final_status = "PARTIAL" - else: - final_status = "OK" - - # Save detailed pipeline execution log to session folder - _save_pipeline_execution_log(state, final_status) - - # In hook_mode, Step 8 (Final Telemetry & Summary Report) doesn't run. Save a quick summary anyway. - if not state.get(StepKeys.SUMMARY_SAVED): - session_dir = state.get(StepKeys.SESSION_DIR) or state.get(StepKeys.SESSION_PATH, "") - if session_dir: - try: - from datetime import datetime - - quick_summary = ( - f"Pipeline: {final_status} | " - f"Task: {state.get(StepKeys.TASK_TYPE, '?')} | " - f"Complexity: {state.get(StepKeys.COMPLEXITY, '?')}/10 | " - f"Skill: {state.get(StepKeys.SKILL, 'none')} | " - f"Agent: {state.get(StepKeys.AGENT, 'none')} | " - f"Framework: {state.get(StepKeys.DETECTED_FRAMEWORK, '?')} | " - f"Time: {datetime.now().strftime('%H:%M:%S')}" - ) - summary_file = Path(session_dir) / "execution-summary.txt" - summary_file.write_text(quick_summary, encoding="utf-8") - except OSError as exc: - logger.debug(f"[output] execution-summary write skipped: {exc}") - - # SESSION ACCUMULATE - Record this request's data via MCP session tools - try: - _src_mcp_dir = Path(__file__).resolve().parent.parent.parent / "src" / "mcp" - if str(_src_mcp_dir) not in sys.path: - sys.path.insert(0, str(_src_mcp_dir)) - from session_hooks import accumulate_request - - accumulate_request( - session_id=state.get(StepKeys.SESSION_ID, ""), - prompt=state.get(StepKeys.USER_MESSAGE, "")[:300], - task_type=state.get(StepKeys.TASK_TYPE, ""), - skill=state.get(StepKeys.SKILL, ""), - complexity=int(state.get(StepKeys.COMPLEXITY, 0)), - model=state.get(StepKeys.SELECTED_MODEL, ""), - cwd=state.get(StepKeys.PROJECT_ROOT, ""), - plan_mode=bool(state.get(StepKeys.PLAN_REQUIRED)), - context_pct=int(state.get("context_pct", 0)), - supplementary_skills=",".join(state.get(StepKeys.SKILLS, []) or []), - ) - except Exception as exc: - logger.debug(f"[output] session accumulate skipped: {exc}") - - # Return synthesis result with proper status - return { - "final_status": final_status, - "synthesized_prompt": synthesis.get("synthesized_prompt", ""), - "synthesis_metadata": synthesis.get("synthesis_metadata", {}), - } - - -def _save_pipeline_execution_log(state: FlowState, final_status: str) -> None: - """Save a detailed pipeline execution log to session folder. - - Creates a human-readable execution-log.md in the session directory showing: - - Each step's status, duration, decisions - - What came in, what went out - - Total pipeline timing - - Skills/agents selected - """ - from datetime import datetime - - session_dir = state.get(StepKeys.SESSION_DIR) or state.get(StepKeys.SESSION_PATH, "") - if not session_dir: - return - - try: - log_lines = [] - log_lines.append("# Pipeline Execution Log") - log_lines.append(f"\n**Session**: {state.get(StepKeys.SESSION_ID, 'unknown')}") - log_lines.append(f"**Date**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") - log_lines.append(f"**Project**: {state.get(StepKeys.PROJECT_ROOT, '.')}") - log_lines.append(f"**Final Status**: {final_status}") - log_lines.append(f"**Framework**: {state.get(StepKeys.DETECTED_FRAMEWORK, 'unknown')}") - log_lines.append("") - - # Level 0: Pre-Flight Sanity Guard - log_lines.append("## Level 0: Pre-Flight Sanity Guard") - log_lines.append(f"- Unicode: {'PASS' if state.get(StepKeys.UNICODE_CHECK) else 'FAIL'}") - log_lines.append(f"- Encoding: {'PASS' if state.get(StepKeys.ENCODING_CHECK) else 'FAIL'}") - log_lines.append(f"- Paths: {'PASS' if state.get(StepKeys.WINDOWS_PATH_CHECK) else 'FAIL'}") - log_lines.append(f"- Status: {state.get(StepKeys.PREFLIGHT_STATUS, 'unknown')}") - log_lines.append("") - - # Level 1: Session & Context Synchronization - log_lines.append("## Level 1: Session & Context Synchronization") - log_lines.append(f"- Session: {state.get(StepKeys.SESSION_ID, 'none')}") - log_lines.append(f"- Complexity: {state.get(StepKeys.COMPLEXITY_SCORE, '?')}/10") - graph_score = state.get(StepKeys.GRAPH_COMPLEXITY_SCORE) - if graph_score: - log_lines.append(f"- Graph Complexity: {graph_score}/25") - combined = state.get(StepKeys.COMBINED_COMPLEXITY_SCORE) - if combined: - log_lines.append(f"- Combined Score: {combined}/25") - log_lines.append(f"- Context Files: {state.get(StepKeys.FILES_LOADED_COUNT, 0)}") - log_lines.append(f"- Cache Hit: {state.get(StepKeys.CONTEXT_CACHE_HIT, False)}") - log_lines.append("") - - # Standards (always-on, loaded from disk -- not a numbered pipeline level) - log_lines.append("## Standards (loaded from disk)") - log_lines.append(f"- Standards Loaded: {state.get(StepKeys.STANDARDS_COUNT, 0)}") - log_lines.append(f"- Framework Detected: {state.get(StepKeys.DETECTED_FRAMEWORK, 'unknown')}") - log_lines.append(f"- MCP Discovered: {state.get(StepKeys.MCP_DISCOVERED_COUNT, 0)}") - log_lines.append(f"- Tool Rules: {'Loaded' if state.get(StepKeys.TOOL_OPTIMIZATION_LOADED) else 'Missing'}") - log_lines.append("") - - # Level 2: SDLC Execution Core (Steps 0-8) - log_lines.append("## Level 2: SDLC Execution Core") - log_lines.append("") - log_lines.append("| Step | Name | Status | Duration | Details |") - log_lines.append("|------|------|--------|----------|---------|") - - step_info = [ - (0, "Pre-Analysis & CallGraph Scan", "step1_callgraph_available", "step1_files_read"), - (1, "Task Orchestration & Planning", "step1_task_type", "step1_complexity"), - (2, "Issue Tracking", "step1_status", "step1_issue_url"), - (3, "Branch & Workspace Setup", "step1_status", "step1_branch_name"), - (4, "Implementation & Code Generation", "step1_implementation_status", "step1_llm_invoked"), - (5, "Pull Request & Automated Review", "step1_status", "step1_pr_url"), - (6, "Issue & Ticket Closure", "step1_status", "step1_issue_closed"), - (7, "Documentation & UML Generation", "step1_documentation_status", "step1_update_count"), - (8, "Final Telemetry & Summary Report", "step2_status", "step2_voice_sent"), - ] - - for step_num, name, status_key, detail_key in step_info: - time_key = f"step{step_num}_execution_time_ms" - duration_ms = state.get(time_key, 0) - duration_str = f"{duration_ms:.0f}ms" if duration_ms else "-" - - status_val = state.get(status_key, "") - detail_val = state.get(detail_key, "") - - # Format detail value - if isinstance(detail_val, (list, dict)): - detail_str = f"{len(detail_val)} items" if isinstance(detail_val, list) else f"{len(detail_val)} keys" - elif isinstance(detail_val, bool): - detail_str = "Yes" if detail_val else "No" - elif detail_val is not None: - detail_str = str(detail_val)[:60] - else: - detail_str = "-" - - status_str = str(status_val)[:30] if status_val else "-" - log_lines.append(f"| {step_num:2d} | {name} | {status_str} | {duration_str} | {detail_str} |") - - log_lines.append("") - - # Selected resources - log_lines.append("## Selected Resources") - skills = state.get(StepKeys.SKILLS) or [] - skill = state.get(StepKeys.SKILL, "") - if skill and skill not in skills: - skills = [skill] + skills - agents = state.get(StepKeys.AGENTS) or [] - agent = state.get(StepKeys.AGENT, "") - if agent and agent not in agents: - agents = [agent] + agents - - if skills: - log_lines.append(f"- **Skills**: {', '.join(skills)}") - if agents: - log_lines.append(f"- **Agents**: {', '.join(agents)}") - log_lines.append("") - - # Errors - errors = state.get(StepKeys.ERRORS) or [] - if errors: - log_lines.append("## Errors") - for err in errors[:10]: - log_lines.append(f"- {err}") - log_lines.append("") - - # Write to session folder - log_path = Path(session_dir) / "execution-log.md" - log_path.write_text("\n".join(log_lines), encoding="utf-8") - - except OSError as exc: - logger.debug(f"[output] execution-log write skipped: {exc}") diff --git a/langgraph_engine/helper_nodes/standards_helpers.py b/langgraph_engine/helper_nodes/standards_helpers.py deleted file mode 100644 index 64b8659d..00000000 --- a/langgraph_engine/helper_nodes/standards_helpers.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Standards helper nodes - Level 2 standards selection and integration hooks. - -Extracted from orchestrator.py. Handles auto-detection of project type/framework -and loading all applicable standards with priority-based conflict resolution. -Also contains standards integration hook functions for Level 3 steps. - -CHANGE LOG (v1.15.2): - Removed apply_integration_step1 through apply_integration_step7 - (Steps 1-7 removed from the pipeline in v1.13.0; these hooks had zero - live callers in the graph). -""" - -from ..flow_state import FlowState -from ..standards_integration import apply_standards_at_step - - -def apply_integration_step10(state: FlowState) -> dict: - """Apply standards integration point at Step 10 (code review).""" - updated = apply_standards_at_step(10, dict(state)) - return {k: updated[k] for k in updated if k not in state or updated[k] != state.get(k)} - - -def apply_integration_step13(state: FlowState) -> dict: - """Apply standards integration point at Step 13 (documentation).""" - updated = apply_standards_at_step(13, dict(state)) - return {k: updated[k] for k in updated if k not in state or updated[k] != state.get(k)} diff --git a/langgraph_engine/helper_nodes/step_helpers.py b/langgraph_engine/helper_nodes/step_helpers.py deleted file mode 100644 index d067d7e5..00000000 --- a/langgraph_engine/helper_nodes/step_helpers.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Step helper nodes - Per-step utility nodes for Level 3 execution. - -Extracted from orchestrator.py. Contains nodes that manage step-level -state transitions (e.g., retry counter increments). -""" - -from ..flow_state import FlowState, StepKeys - - -def step5_retry_increment_node(state: FlowState) -> dict: - """Increment retry count before re-routing to Step 10. - - State mutations must happen in nodes, not routing functions (LangGraph anti-pattern). - """ - retry_count = state.get(StepKeys.RETRY_COUNT, 0) - return {StepKeys.RETRY_COUNT: retry_count + 1} diff --git a/langgraph_engine/orchestrator.py b/langgraph_engine/orchestrator.py index 43cd10a2..3abcce05 100644 --- a/langgraph_engine/orchestrator.py +++ b/langgraph_engine/orchestrator.py @@ -145,9 +145,9 @@ def step5_retry_increment_node(state: FlowState) -> dict: def optimize_context_after_level1(state: FlowState) -> dict: - """Optimize context after Level 1 completes before passing to Level 3. + """Optimize context after Level 1 completes before passing to Level 2. - Stores full Level 1 output in workflow_memory, passes only summary to Level 3. + Stores full Level 1 output in workflow_memory, passes only summary to Level 2. This keeps context clean while preserving full data for fallback/debugging. """ # Store full level 1 output @@ -161,7 +161,7 @@ def optimize_context_after_level1(state: FlowState) -> dict: state = WorkflowContextOptimizer.store_step_output(state, "level1_output", level1_output) - # Build optimized context for Level 3 + # Build optimized context for Level 2 optimized = WorkflowContextOptimizer.build_optimized_context(state) state["workflow_context_optimized"] = optimized @@ -191,7 +191,7 @@ def save_workflow_memory(state: FlowState) -> dict: memory = state.get(StepKeys.WORKFLOW_MEMORY, {}) if memory and session_id != "unknown": - # Save to ~/.claude/memory/sessions/{session_id}/workflow-memory.json + # Save to ~/.claude/memory/logs/sessions/{session_id}/workflow-memory.json memory_dir = _ORCHESTRATOR_CLAUDE_HOME / "memory" / "logs" / "sessions" / session_id memory_dir.mkdir(parents=True, exist_ok=True)