diff --git a/AGENTS.md b/AGENTS.md index 4bec1f28..f9b6b066 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,35 +23,4 @@ This repository uses a hierarchical, decentralized protocol system. Each of the ## Root Protocols -General protocols are defined in the [root protocol module](./protocols/AGENTS.md). -# --- Child Protocol Modules --- - -This repository uses a hierarchical protocol system. Each of the following directories contains a self-contained set of protocols in its own `AGENTS.md` file. -- [Compliance](protocols/compliance/AGENTS.md) -- [Core](protocols/core/AGENTS.md) -- [Critic](protocols/critic/AGENTS.md) -- [Security](protocols/security/AGENTS.md) -- [Self-improvement](protocols/self-improvement/AGENTS.md) -- [Testing](protocols/testing/AGENTS.md) -- [Self_improvement](protocols/self_improvement/AGENTS.md) - - -# Jules' Plan - -1. *Create a `jules_agent` directory in `tooling`.* This will house the logic for my agent's operations. -2. *Implement a `plan_manager.py` in `tooling/jules_agent`.* This module will be responsible for creating, updating, and storing the plan in the correct `AGENTS.md` file based on the current working directory. -3. *Implement an `action_logger.py` in `tooling/jules_agent`.* This module will be responsible for logging actions to the correct `AGENTS.md` file. -4. *Integrate the `plan_manager` and `action_logger` into my workflow.* I will call these tools to update the `AGENTS.md` files as I work. -5. *Complete pre commit steps* - - Complete pre commit steps to make sure proper testing, verifications, reviews and reflections are done. -6. *Submit the change.* - - Once the new `AGENTS.md` is generated and integrated, I will submit the change. - - - -# Jules' Action Log - -- 2025-10-18 22:04:02: Created `tooling/jules_agent` directory. -- 2025-10-18 22:04:02: Implemented `plan_manager.py`. -- 2025-10-18 22:04:02: Implemented `action_logger.py`. -- 2025-10-18 22:04:02: Injected the current plan into the root AGENTS.md. +General protocols are defined in the [root protocol module](./protocols/AGENTS.md). \ No newline at end of file diff --git a/knowledge_core/dependency_graph.json b/knowledge_core/dependency_graph.json index a5f1c38f..7e3e0569 100644 --- a/knowledge_core/dependency_graph.json +++ b/knowledge_core/dependency_graph.json @@ -198,7 +198,7 @@ "target": "ply" }, { - "source": "root-python-.project", + "source": "root-python-project", "target": "black" }, { diff --git a/requirements.txt b/requirements.txt index edac3992..00d6000e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,3 +20,10 @@ markdown ply black flake8 +jsonschema +rdflib +markdown +pypdf +requests +watchdog +ply diff --git a/tooling/agent_shell.py b/tooling/agent_shell.py index d11bcbf3..aaf843c6 100644 --- a/tooling/agent_shell.py +++ b/tooling/agent_shell.py @@ -13,7 +13,6 @@ import uuid import os import sys -import subprocess # Add the root directory to the path to allow for absolute imports sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) @@ -38,16 +37,6 @@ def run_agent_loop(task_description: str, tools: dict, model: str = None): """ The main loop that drives the agent's lifecycle via the FSM. """ - # Install dependencies from requirements.txt - try: - print("[AgentShell] Installing dependencies from requirements.txt...") - subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"]) - print("[AgentShell] Dependencies installed successfully.") - except subprocess.CalledProcessError as e: - print(f"[AgentShell] Error installing dependencies: {e}") - # Depending on the desired behavior, you might want to exit or handle the error - # For now, we'll just print the error and continue. - # 1. Initialize State and Logger task_id = f"task-{uuid.uuid4()}" agent_state = AgentState(task=task_id) diff --git a/tooling/capability_verifier.py b/tooling/capability_verifier.py index 6ba87257..422d5d9a 100644 --- a/tooling/capability_verifier.py +++ b/tooling/capability_verifier.py @@ -25,10 +25,6 @@ import argparse import subprocess import sys -import os -import tempfile -import shutil -import json def main(): """ @@ -62,33 +58,29 @@ def main(): # Step 2: Create a lesson and invoke the self-correction orchestrator print("\n--- Step 2: Invoking self-correction ---") - temp_dir = tempfile.mkdtemp() - try: - lesson_content = { - "lesson_id": "verify-fibonacci-capability", - "status": "pending", - "failure": { - "test_file": args.test_file, - "error_message": initial_result.stderr - }, - "action": { - "type": "PROPOSE_CODE_CHANGE", - "parameters": { - "filepath": "self_improvement_project/main.py", - "diff": "No-op for this test, as the fix is already applied." - } + lesson_content = { + "lesson_id": "verify-fibonacci-capability", + "status": "pending", + "failure": { + "test_file": args.test_file, + "error_message": initial_result.stderr + }, + "action": { + "type": "PROPOSE_CODE_CHANGE", + "parameters": { + "filepath": "self_improvement_project/main.py", + "diff": "No-op for this test, as the fix is already applied." } } - lesson_file_path = os.path.join(temp_dir, "lessons.jsonl") - with open(lesson_file_path, "w") as f: - f.write(json.dumps(lesson_content) + "\n") + } + import json + with open("knowledge_core/lessons.jsonl", "w") as f: + f.write(json.dumps(lesson_content) + "\n") - orchestrator_result = subprocess.run( - [sys.executable, "tooling/self_correction_orchestrator.py", f"--lesson-file={lesson_file_path}"], - capture_output=True, text=True - ) - finally: - shutil.rmtree(temp_dir) + orchestrator_result = subprocess.run( + [sys.executable, "tooling/self_correction_orchestrator.py"], + capture_output=True, text=True + ) if orchestrator_result.returncode != 0: print("Error: Self-correction orchestrator failed.") print(orchestrator_result.stderr) diff --git a/tooling/jules_agent/action_logger.py b/tooling/jules_agent/action_logger.py deleted file mode 100644 index d1eb01b2..00000000 --- a/tooling/jules_agent/action_logger.py +++ /dev/null @@ -1,39 +0,0 @@ -import os -import re -from datetime import datetime - -def get_agents_md_path(cwd): - """Finds the AGENTS.md file in the given directory.""" - path = os.path.join(cwd, "AGENTS.md") - if os.path.exists(path): - return path - return None - -def log_action(action_text, cwd): - """Logs an action to the AGENTS.md file.""" - agents_md_path = get_agents_md_path(cwd) - if not agents_md_path: - # Create AGENTS.md if it doesn't exist - with open(os.path.join(cwd, "AGENTS.md"), "w") as f: - f.write("# Jules' Action Log\n\n- " + action_text) - return - - with open(agents_md_path, "r") as f: - content = f.read() - - log_header = "# Jules' Action Log" - log_section_regex = re.compile(r"(^# Jules' Action Log\n)(.*?)(^#|\Z)", re.MULTILINE | re.DOTALL) - match = log_section_regex.search(content) - - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - log_entry = f"- {timestamp}: {action_text}\n" - - if match: - # Append to existing log - new_content = content.replace(match.group(2), match.group(2) + log_entry) - else: - # Add new log section - new_content = content + "\n\n" + log_header + "\n\n" + log_entry - - with open(agents_md_path, "w") as f: - f.write(new_content) diff --git a/tooling/jules_agent/plan_manager.py b/tooling/jules_agent/plan_manager.py deleted file mode 100644 index 3613e33a..00000000 --- a/tooling/jules_agent/plan_manager.py +++ /dev/null @@ -1,35 +0,0 @@ -import os -import re - -def get_agents_md_path(cwd): - """Finds the AGENTS.md file in the given directory.""" - path = os.path.join(cwd, "AGENTS.md") - if os.path.exists(path): - return path - return None - -def inject_plan(plan_text, cwd): - """Injects or updates the plan in the AGENTS.md file.""" - agents_md_path = get_agents_md_path(cwd) - if not agents_md_path: - # Create AGENTS.md if it doesn't exist - with open(os.path.join(cwd, "AGENTS.md"), "w") as f: - f.write("# Jules' Plan\n\n" + plan_text) - return - - with open(agents_md_path, "r") as f: - content = f.read() - - plan_header = "# Jules' Plan" - plan_section_regex = re.compile(r"(^# Jules' Plan\n)(.*?)(^#|\Z)", re.MULTILINE | re.DOTALL) - match = plan_section_regex.search(content) - - if match: - # Update existing plan - new_content = content.replace(match.group(2), "\n" + plan_text + "\n\n") - else: - # Add new plan section - new_content = content + "\n\n" + plan_header + "\n\n" + plan_text - - with open(agents_md_path, "w") as f: - f.write(new_content) diff --git a/tooling/protocol_api.py b/tooling/protocol_api.py new file mode 100644 index 00000000..49718d3b --- /dev/null +++ b/tooling/protocol_api.py @@ -0,0 +1,24 @@ +from tooling.protocol_manager import create_protocol, update_version +from tooling.protocol_oracle import get_applicable_protocols, get_rules_for_protocols +from rdflib import Graph +import os + +class ProtocolAPI: + def __init__(self, protocols_dir="protocols", knowledge_graph_path="knowledge_core/protocols.ttl"): + self.protocols_dir = protocols_dir + self.knowledge_graph_path = knowledge_graph_path + self.graph = Graph() + if os.path.exists(self.knowledge_graph_path): + self.graph.parse(self.knowledge_graph_path, format="turtle") + + def create_protocol(self, name, directory=None): + if directory is None: + directory = self.protocols_dir + create_protocol(name, directory) + + def update_protocol_version(self, protocol_id, new_version): + update_version(protocol_id, new_version) + + def get_applicable_rules(self, context): + applicable_protocols = get_applicable_protocols(self.graph, context) + return get_rules_for_protocols(self.graph, applicable_protocols) diff --git a/tooling/protocol_compiler.py b/tooling/protocol_compiler.py index bfbb3080..fcbe7db8 100644 --- a/tooling/protocol_compiler.py +++ b/tooling/protocol_compiler.py @@ -174,7 +174,12 @@ def compile_single_module(source_dir, target_file, schema_file, knowledge_graph= os.rename(temp_target_file, target_file) def compile_module_wrapper(path_to_protocol_dir): + """Wrapper to execute the local build script in a given protocol directory.""" local_builder = os.path.join(path_to_protocol_dir, LOCAL_BUILD_SCRIPT_NAME) + if not os.path.exists(local_builder): + # This is not an error, it just means it's a directory with no build script. + # It might just contain other protocol modules. + return path_to_protocol_dir, True # Return True as there was no failure. try: print(f"--- Executing local builder: {local_builder} ---") subprocess.check_call([sys.executable, local_builder], cwd=path_to_protocol_dir, stdout=sys.stdout, stderr=sys.stderr) @@ -246,28 +251,32 @@ def main_orchestrator(): print("--- Starting Parallel Protocol Build Orchestration ---") start_time = time.time() - # find_protocol_dirs should be adapted to find dirs containing 'build.py' - all_dirs = find_protocol_dirs(ROOT_PROTOCOLS_DIR) - - build_script_dirs = [d for d in all_dirs if os.path.exists(os.path.join(d, LOCAL_BUILD_SCRIPT_NAME))] - - if not build_script_dirs: - print("No protocol directories with a 'build.py' script found. Exiting.", file=sys.stderr) + all_protocol_dirs = find_protocol_dirs(ROOT_PROTOCOLS_DIR) + if not all_protocol_dirs: + print("No protocol directories found. Exiting.") return successful_compilations = [] failed_compilations = [] + with ThreadPoolExecutor() as executor: - future_to_dir = {executor.submit(compile_module_wrapper, dir_path): dir_path for dir_path in build_script_dirs} + future_to_dir = {executor.submit(compile_module_wrapper, dir_path): dir_path for dir_path in all_protocol_dirs} for future in as_completed(future_to_dir): path, success = future.result() - if not success: - print(f"\n--- Compilation failed for: {get_protocol_dir_name(path)} ---") - sys.exit(1) + if success: + successful_compilations.append(path) + else: + failed_compilations.append(path) + + if failed_compilations: + print("\n--- Compilation Summary: Failures Detected ---") + for path in failed_compilations: + print(f"- {get_protocol_dir_name(path)}") + sys.exit(1) print("\n--- All protocol modules compiled successfully. ---") - generate_root_agents_md(build_script_dirs) + generate_root_agents_md(successful_compilations) print("\n--- Parallel Protocol Build Orchestration Finished ---") diff --git a/tooling/requirements.txt b/tooling/requirements.txt deleted file mode 100644 index 6106ce57..00000000 --- a/tooling/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -jsonschema -rdflib -markdown -pypdf -requests -watchdog -ply \ No newline at end of file diff --git a/tooling/self_correction_orchestrator.py b/tooling/self_correction_orchestrator.py index fb4e8519..25865429 100644 --- a/tooling/self_correction_orchestrator.py +++ b/tooling/self_correction_orchestrator.py @@ -9,25 +9,24 @@ import json import os import subprocess -import argparse LESSONS_FILE = "knowledge_core/lessons.jsonl" UPDATER_SCRIPT = "tooling/protocol_updater.py" CODE_SUGGESTER_SCRIPT = "tooling/code_suggester.py" -def load_lessons(lesson_file): +def load_lessons(): """Loads all lessons from the JSONL file.""" - if not os.path.exists(lesson_file): + if not os.path.exists(LESSONS_FILE): return [] - with open(lesson_file, "r") as f: + with open(LESSONS_FILE, "r") as f: return [json.loads(line) for line in f] -def save_lessons(lessons, lesson_file): +def save_lessons(lessons): """Saves a list of lessons back to the JSONL file, overwriting it.""" - with open(lesson_file, "w") as f: + with open(LESSONS_FILE, "w") as f: for lesson in lessons: f.write(json.dumps(lesson) + "\n") @@ -161,18 +160,13 @@ def main(): """ Main function to run the self-correction workflow. """ - parser = argparse.ArgumentParser(description="Orchestrate the Protocol-Driven Self-Correction (PDSC) workflow.") - parser.add_argument( - "--lesson-file", - default=LESSONS_FILE, - help="Path to the lessons JSONL file." - ) - args = parser.parse_args() - + # This script is intended to be called from a controlled environment + # like a test or a dedicated plan, so we don't use argparse here. + # The search for protocols should start from the repository root. protocols_directory = "." print("--- Starting Protocol-Driven Self-Correction Cycle ---") - lessons = load_lessons(args.lesson_file) + lessons = load_lessons() if not any(lesson.get("status") == "pending" for lesson in lessons): print("No pending lessons to process. Exiting.") @@ -181,7 +175,7 @@ def main(): changes_were_applied = process_lessons(lessons, protocols_directory) print("\n--- Saving updated lesson statuses ---") - save_lessons(lessons, args.lesson_file) + save_lessons(lessons) if changes_were_applied: print("\n--- Protocol sources updated. Rebuilding AGENTS.md... ---") diff --git a/tooling/test_protocol_api.py b/tooling/test_protocol_api.py new file mode 100644 index 00000000..73118173 --- /dev/null +++ b/tooling/test_protocol_api.py @@ -0,0 +1,52 @@ +import unittest +import os +import shutil +from unittest.mock import patch, MagicMock +from rdflib import Graph +import sys + +# Add the root directory to the Python path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from tooling.protocol_api import ProtocolAPI + +class TestProtocolAPI(unittest.TestCase): + + def setUp(self): + self.test_dir = "test_protocol_api_dir" + os.makedirs(self.test_dir, exist_ok=True) + self.protocols_dir = os.path.join(self.test_dir, "protocols") + os.makedirs(self.protocols_dir, exist_ok=True) + self.knowledge_graph_path = os.path.join(self.test_dir, "protocols.ttl") + + def tearDown(self): + shutil.rmtree(self.test_dir) + + @patch("tooling.protocol_api.create_protocol") + def test_create_protocol(self, mock_create_protocol): + api = ProtocolAPI(protocols_dir=self.protocols_dir) + api.create_protocol("Test Protocol") + mock_create_protocol.assert_called_once_with("Test Protocol", self.protocols_dir) + + @patch("tooling.protocol_api.update_version") + def test_update_protocol_version(self, mock_update_version): + api = ProtocolAPI() + api.update_protocol_version("test-protocol", "1.1.0") + mock_update_version.assert_called_once_with("test-protocol", "1.1.0") + + @patch("tooling.protocol_api.get_applicable_protocols") + @patch("tooling.protocol_api.get_rules_for_protocols") + def test_get_applicable_rules(self, mock_get_rules, mock_get_protocols): + api = ProtocolAPI(knowledge_graph_path=self.knowledge_graph_path) + context = {"task_type": "refactor"} + mock_get_protocols.return_value = ["proto1", "proto2"] + mock_get_rules.return_value = ["rule1", "rule2"] + + rules = api.get_applicable_rules(context) + + mock_get_protocols.assert_called_once_with(api.graph, context) + mock_get_rules.assert_called_once_with(api.graph, ["proto1", "proto2"]) + self.assertEqual(rules, ["rule1", "rule2"]) + +if __name__ == "__main__": + unittest.main() diff --git a/tooling/test_protocol_manager.py b/tooling/test_protocol_manager.py new file mode 100644 index 00000000..4e2b7326 --- /dev/null +++ b/tooling/test_protocol_manager.py @@ -0,0 +1,57 @@ +import unittest +import os +import json +import shutil +from unittest.mock import patch +import sys + +# Add the root directory to the Python path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from tooling.protocol_manager import create_protocol, update_version, run_tests + +class TestProtocolManager(unittest.TestCase): + + def setUp(self): + self.test_dir = "test_protocol_manager_dir" + os.makedirs(self.test_dir, exist_ok=True) + + def tearDown(self): + shutil.rmtree(self.test_dir) + + def test_create_protocol(self): + create_protocol("Test Protocol", self.test_dir) + protocol_file = os.path.join(self.test_dir, "test-protocol.protocol.json") + self.assertTrue(os.path.exists(protocol_file)) + with open(protocol_file, "r") as f: + data = json.load(f) + self.assertEqual(data["protocol_id"], "test-protocol") + self.assertEqual(data["version"], "1.0.0") + + def test_update_version(self): + protocol_file = os.path.join(self.test_dir, "test-protocol.protocol.json") + with open(protocol_file, "w") as f: + json.dump({"protocol_id": "test-protocol", "version": "1.0.0"}, f) + + # To make update_version work, we need to be in a directory that has a `protocols` subdirectory. + # So we'll create one. + os.makedirs(os.path.join(self.test_dir, "protocols"), exist_ok=True) + shutil.move(protocol_file, os.path.join(self.test_dir, "protocols", "test-protocol.protocol.json")) + + # We also need to change the current working directory + original_cwd = os.getcwd() + os.chdir(self.test_dir) + update_version("test-protocol", "1.1.0") + os.chdir(original_cwd) + + with open(os.path.join(self.test_dir, "protocols", "test-protocol.protocol.json"), "r") as f: + data = json.load(f) + self.assertEqual(data["version"], "1.1.0") + + @patch("os.system") + def test_run_tests(self, mock_system): + run_tests() + mock_system.assert_called_once_with("python3 tests/protocols/test_runner.py") + +if __name__ == "__main__": + unittest.main() diff --git a/tooling/test_protocol_oracle.py b/tooling/test_protocol_oracle.py index 404cb487..bd969025 100644 --- a/tooling/test_protocol_oracle.py +++ b/tooling/test_protocol_oracle.py @@ -60,5 +60,27 @@ def test_get_rules_for_protocols(self): self.assertEqual(len(rules), 1) self.assertEqual(rules[0]["rule_id"], "rule-1") + def test_get_rules_for_protocol_with_no_rules(self): + """Tests that an empty list is returned for a protocol with no rules.""" + p1_uri = PROTOCOL["proto-without-rules"] + self.g.add((p1_uri, RDF.type, PROTOCOL.Protocol)) + + rules = get_rules_for_protocols(self.g, [str(p1_uri)]) + self.assertEqual(len(rules), 0) + + def test_applicability_script_error(self): + """Tests that the system handles errors in the applicability script gracefully.""" + p1_uri = PROTOCOL["error-proto"] + error_script_path = os.path.join(self.test_dir, "error.protocol.py") + with open(error_script_path, "w") as f: + f.write('def is_applicable(context):\n') + f.write(' raise ValueError("This is a test error")\n') + + self.g.add((p1_uri, RDF.type, PROTOCOL.Protocol)) + self.g.add((p1_uri, PROTOCOL.hasApplicabilityCondition, Literal(error_script_path))) + + applicable = get_applicable_protocols(self.g, {}) + self.assertNotIn(str(p1_uri), applicable) + if __name__ == "__main__": unittest.main() \ No newline at end of file diff --git a/tooling/test_self_correction_orchestrator.py b/tooling/test_self_correction_orchestrator.py index f78d25a5..f0ee7bab 100644 --- a/tooling/test_self_correction_orchestrator.py +++ b/tooling/test_self_correction_orchestrator.py @@ -34,14 +34,14 @@ def test_load_and_save_lessons(self): for lesson in lessons_data: f.write(json.dumps(lesson) + "\n") - loaded = load_lessons(self.lessons_file) + loaded = load_lessons() self.assertEqual(len(loaded), 2) self.assertEqual(loaded[0]['lesson_id'], 'L001') loaded[0]['status'] = 'failed' - save_lessons(loaded, self.lessons_file) + save_lessons(loaded) - reloaded = load_lessons(self.lessons_file) + reloaded = load_lessons() self.assertEqual(reloaded[0]['status'], 'failed') @patch('tooling.self_correction_orchestrator.UPDATER_SCRIPT', 'mock_updater.py') @@ -103,10 +103,8 @@ def test_failed_command(self): self.assertEqual(lessons[0]['status'], 'failed') @patch('tooling.self_correction_orchestrator.process_lessons', return_value=True) - @patch('argparse.ArgumentParser.parse_args') - def test_main_flow_rebuilds_agents_md(self, mock_parse_args, mock_process): + def test_main_flow_rebuilds_agents_md(self, mock_process): """Tests that the main function calls to rebuild AGENTS.md after changes.""" - mock_parse_args.return_value = MagicMock(lesson_file=self.lessons_file) with open(self.lessons_file, "w") as f: f.write(json.dumps({"lesson_id": "L001", "status": "pending"}) + "\n") @@ -118,10 +116,8 @@ def test_main_flow_rebuilds_agents_md(self, mock_parse_args, mock_process): # Check that 'make AGENTS.md' was called self.mock_run_command.assert_called_with(["make", "AGENTS.md"]) - @patch('argparse.ArgumentParser.parse_args') - def test_no_pending_lessons(self, mock_parse_args): + def test_no_pending_lessons(self): """Tests that the orchestrator does nothing if there are no pending lessons.""" - mock_parse_args.return_value = MagicMock(lesson_file=self.lessons_file) lessons = [{"lesson_id": "L001", "status": "applied"}] with open(self.lessons_file, "w") as f: for lesson in lessons: