diff --git a/.gitignore b/.gitignore index 608225ab68..20c57982de 100644 --- a/.gitignore +++ b/.gitignore @@ -164,3 +164,6 @@ hive-mind-prompt-*.txt .cleveragents/ .pabotsuitenames PIPE + +# Git worktrees for parallel task branches +worktrees/ diff --git a/benchmarks/decision_model_bench.py b/benchmarks/decision_model_bench.py new file mode 100644 index 0000000000..1ced34bfbd --- /dev/null +++ b/benchmarks/decision_model_bench.py @@ -0,0 +1,192 @@ +"""ASV benchmarks for decision domain model operations. + +Measures the performance of: +- Decision model construction (Pydantic validation) +- DecisionType enum access +- ContextSnapshot construction +- Decision serialization (model_dump / model_validate) +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +try: + from cleveragents.domain.models.core.decision import ( + ArtifactRef, + ContextSnapshot, + Decision, + DecisionType, + ResourceRef, + ) +except ModuleNotFoundError: + sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) + from cleveragents.domain.models.core.decision import ( + ArtifactRef, + ContextSnapshot, + Decision, + DecisionType, + ResourceRef, + ) + +from ulid import ULID + +_PLAN_ID = str(ULID()) +_PARENT_ID = str(ULID()) +_RESOURCE_ID = str(ULID()) + + +class DecisionConstructionSuite: + """Benchmark Decision model construction.""" + + def time_minimal_root_decision(self) -> None: + """Benchmark minimal prompt_definition creation.""" + Decision( + plan_id=_PLAN_ID, + sequence_number=0, + decision_type=DecisionType.PROMPT_DEFINITION, + question="What should we build?", + chosen_option="A REST API", + ) + + def time_child_decision_with_confidence(self) -> None: + """Benchmark child decision with confidence score.""" + Decision( + plan_id=_PLAN_ID, + parent_decision_id=_PARENT_ID, + sequence_number=1, + decision_type=DecisionType.STRATEGY_CHOICE, + question="Which framework?", + chosen_option="FastAPI", + alternatives_considered=["Flask", "Django", "Starlette"], + confidence_score=0.85, + ) + + def time_decision_with_snapshot(self) -> None: + """Benchmark decision with full context snapshot.""" + Decision( + plan_id=_PLAN_ID, + parent_decision_id=_PARENT_ID, + sequence_number=2, + decision_type=DecisionType.IMPLEMENTATION_CHOICE, + question="How to implement auth?", + chosen_option="JWT tokens", + context_snapshot=ContextSnapshot( + hot_context_hash="sha256:bench123", + hot_context_ref="store://snapshots/bench123", + relevant_resources=[ + ResourceRef(resource_id=_RESOURCE_ID, path="src/main.py"), + ResourceRef(resource_id=str(ULID())), + ], + actor_state_ref="checkpoint://actor/bench", + ), + ) + + def time_correction_decision(self) -> None: + """Benchmark correction decision creation.""" + Decision( + plan_id=_PLAN_ID, + parent_decision_id=_PARENT_ID, + sequence_number=3, + decision_type=DecisionType.STRATEGY_CHOICE, + question="Which framework?", + chosen_option="Django instead", + is_correction=True, + corrects_decision_id=str(ULID()), + correction_reason="Changed requirements", + ) + + def time_decision_with_artifacts(self) -> None: + """Benchmark decision with artifacts list.""" + Decision( + plan_id=_PLAN_ID, + parent_decision_id=_PARENT_ID, + sequence_number=4, + decision_type=DecisionType.TOOL_INVOCATION, + question="Which tool?", + chosen_option="code_editor", + artifacts_produced=[ + ArtifactRef(artifact_path=f"src/file_{i}.py") for i in range(10) + ], + ) + + +class DecisionSerializationSuite: + """Benchmark Decision serialization round-trips.""" + + def setup(self) -> None: + self.decision = Decision( + plan_id=_PLAN_ID, + parent_decision_id=_PARENT_ID, + sequence_number=5, + decision_type=DecisionType.STRATEGY_CHOICE, + question="Architecture choice?", + chosen_option="Microservices", + alternatives_considered=["Monolith", "Serverless"], + confidence_score=0.78, + context_snapshot=ContextSnapshot( + hot_context_hash="sha256:serial", + hot_context_ref="store://serial", + relevant_resources=[ + ResourceRef(resource_id=_RESOURCE_ID, path="src/app.py"), + ], + actor_state_ref="checkpoint://serial", + ), + artifacts_produced=[ + ArtifactRef(artifact_path="src/service.py"), + ], + ) + self.dump = self.decision.model_dump() + + def time_model_dump(self) -> None: + """Benchmark model_dump serialization.""" + self.decision.model_dump() + + def time_model_dump_json(self) -> None: + """Benchmark model_dump_json serialization.""" + self.decision.model_dump_json() + + def time_model_validate(self) -> None: + """Benchmark model_validate deserialization.""" + Decision.model_validate(self.dump) + + def time_as_cli_dict(self) -> None: + """Benchmark as_cli_dict rendering.""" + self.decision.as_cli_dict() + + +class ContextSnapshotSuite: + """Benchmark ContextSnapshot construction.""" + + def time_empty_snapshot(self) -> None: + """Benchmark empty snapshot creation.""" + ContextSnapshot() + + def time_snapshot_with_resources(self) -> None: + """Benchmark snapshot with multiple resource refs.""" + ContextSnapshot( + hot_context_hash="sha256:bench", + hot_context_ref="store://bench", + relevant_resources=[ + ResourceRef(resource_id=str(ULID()), path=f"src/f{i}.py") + for i in range(20) + ], + actor_state_ref="checkpoint://bench", + ) + + +class DecisionTypeEnumSuite: + """Benchmark DecisionType enum operations.""" + + def time_enum_access(self) -> None: + """Benchmark accessing an enum member.""" + _ = DecisionType.PROMPT_DEFINITION + + def time_enum_from_value(self) -> None: + """Benchmark creating enum from string value.""" + DecisionType("strategy_choice") + + def time_enum_iteration(self) -> None: + """Benchmark iterating all enum members.""" + list(DecisionType) diff --git a/benchmarks/resource_handler_bench.py b/benchmarks/resource_handler_bench.py new file mode 100644 index 0000000000..24f483d92b --- /dev/null +++ b/benchmarks/resource_handler_bench.py @@ -0,0 +1,214 @@ +"""ASV benchmarks for resource handler resolution overhead. + +Measures the performance of: +- Handler resolver (import + cache lookup) +- GitCheckoutHandler.resolve() with mock sandbox +- FsDirectoryHandler.resolve() with mock sandbox +- ResourceHandlerService.resolve_binding() end-to-end +""" + +from __future__ import annotations + +import sys +from datetime import datetime +from pathlib import Path +from unittest.mock import MagicMock, PropertyMock + +try: + from cleveragents.domain.models.core.resource import ( + PhysVirt, + Resource, + ResourceCapabilities, + ) + from cleveragents.domain.models.core.resource_slot import BindingResult + from cleveragents.domain.models.core.resource_type import ( + ResourceKind, + ResourceTypeSpec, + ) + from cleveragents.domain.models.core.resource_type import ( + SandboxStrategy as TypeSandboxStrategy, + ) + from cleveragents.infrastructure.sandbox.factory import SandboxFactory + from cleveragents.infrastructure.sandbox.manager import SandboxManager + from cleveragents.infrastructure.sandbox.protocol import ( + SandboxContext, + SandboxStatus, + ) + from cleveragents.resource.handlers.fs_directory import FsDirectoryHandler + from cleveragents.resource.handlers.git_checkout import GitCheckoutHandler + from cleveragents.resource.handlers.resolver import ( + clear_handler_cache, + resolve_handler, + ) +except ModuleNotFoundError: + sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) + from cleveragents.domain.models.core.resource import ( + PhysVirt, + Resource, + ResourceCapabilities, + ) + from cleveragents.domain.models.core.resource_slot import BindingResult + from cleveragents.domain.models.core.resource_type import ( + ResourceKind, + ResourceTypeSpec, + ) + from cleveragents.domain.models.core.resource_type import ( + SandboxStrategy as TypeSandboxStrategy, + ) + from cleveragents.infrastructure.sandbox.factory import SandboxFactory + from cleveragents.infrastructure.sandbox.manager import SandboxManager + from cleveragents.infrastructure.sandbox.protocol import ( + SandboxContext, + SandboxStatus, + ) + from cleveragents.resource.handlers.fs_directory import FsDirectoryHandler + from cleveragents.resource.handlers.git_checkout import GitCheckoutHandler + from cleveragents.resource.handlers.resolver import ( + clear_handler_cache, + resolve_handler, + ) + + +def _make_resource(rtype: str, location: str) -> Resource: + return Resource( + resource_id="01KJ5C5TPMP8GGX3QC83E2MAQS", + resource_type_name=rtype, + classification=PhysVirt.PHYSICAL, + location=location, + capabilities=ResourceCapabilities( + readable=True, writable=True, sandboxable=True + ), + ) + + +def _make_mock_manager() -> SandboxManager: + mock_factory = MagicMock(spec=SandboxFactory) + mock_sandbox = MagicMock() + mock_sandbox.sandbox_id = "sb-bench-001" + type(mock_sandbox).status = PropertyMock(return_value=SandboxStatus.CREATED) + mock_sandbox.context = SandboxContext( + sandbox_id="sb-bench-001", + sandbox_path="/tmp/sandbox/sb-bench-001", + original_path="/tmp/original", + resource_id="res-bench", + plan_id="plan-bench", + created_at=datetime.now(), + ) + mock_sandbox.create.return_value = mock_sandbox.context + mock_factory.create_sandbox.return_value = mock_sandbox + return SandboxManager(factory=mock_factory, cleanup_on_exit=False) + + +class HandlerResolverSuite: + """Benchmark handler resolver import and cache performance.""" + + def time_resolve_git_handler_cold(self) -> None: + """Benchmark cold resolve (no cache).""" + clear_handler_cache() + resolve_handler( + "cleveragents.resource.handlers.git_checkout:GitCheckoutHandler" + ) + + def time_resolve_git_handler_cached(self) -> None: + """Benchmark cached resolve.""" + resolve_handler( + "cleveragents.resource.handlers.git_checkout:GitCheckoutHandler" + ) + + def time_resolve_fs_handler_cold(self) -> None: + """Benchmark cold resolve for fs-directory.""" + clear_handler_cache() + resolve_handler( + "cleveragents.resource.handlers.fs_directory:FsDirectoryHandler" + ) + + +class GitCheckoutHandlerSuite: + """Benchmark GitCheckoutHandler.resolve() overhead.""" + + def setup(self) -> None: + self.resource = _make_resource("git-checkout", "/tmp/bench-repo") + self.manager = _make_mock_manager() + self.handler = GitCheckoutHandler() + + def time_resolve_git_checkout(self) -> None: + """Benchmark single git-checkout resolution.""" + self.handler.resolve( + resource=self.resource, + plan_id="PLAN_BENCH", + slot_name="repo", + sandbox_manager=self.manager, + ) + + +class FsDirectoryHandlerSuite: + """Benchmark FsDirectoryHandler.resolve() overhead.""" + + def setup(self) -> None: + self.resource = _make_resource("fs-directory", "/tmp/bench-dir") + self.manager = _make_mock_manager() + self.handler = FsDirectoryHandler() + + def time_resolve_fs_directory(self) -> None: + """Benchmark single fs-directory resolution.""" + self.handler.resolve( + resource=self.resource, + plan_id="PLAN_BENCH", + slot_name="workdir", + sandbox_manager=self.manager, + ) + + +class ResourceHandlerServiceSuite: + """Benchmark ResourceHandlerService end-to-end resolution.""" + + def setup(self) -> None: + from cleveragents.application.services.resource_handler_service import ( + ResourceHandlerService, + ) + + self.resource = _make_resource("git-checkout", "/tmp/bench-repo") + self.type_spec = ResourceTypeSpec( + name="git-checkout", + description="Test git-checkout type", + resource_kind=ResourceKind.PHYSICAL, + sandbox_strategy=TypeSandboxStrategy.GIT_WORKTREE, + user_addable=True, + built_in=True, + handler="cleveragents.resource.handlers.git_checkout:GitCheckoutHandler", + ) + self.manager = _make_mock_manager() + self.service = ResourceHandlerService( + sandbox_manager=self.manager, + resource_lookup=lambda _: self.resource, + type_lookup=lambda _: self.type_spec, + ) + self.binding = BindingResult( + slot_name="repo", + resource_id="01KJ5C5TPMP8GGX3QC83E2MAQS", + resource_name="test-repo", + binding_mode="contextual", + ) + + def time_resolve_binding(self) -> None: + """Benchmark single binding resolution.""" + self.service.resolve_binding( + binding=self.binding, + plan_id="PLAN_BENCH", + ) + + def time_resolve_bindings_batch(self) -> None: + """Benchmark batch binding resolution (5 bindings).""" + bindings = [ + BindingResult( + slot_name=f"slot-{i}", + resource_id="01KJ5C5TPMP8GGX3QC83E2MAQS", + resource_name=f"res-{i}", + binding_mode="contextual", + ) + for i in range(5) + ] + self.service.resolve_bindings( + bindings=bindings, + plan_id="PLAN_BENCH", + ) diff --git a/benchmarks/resource_registry_bench.py b/benchmarks/resource_registry_bench.py new file mode 100644 index 0000000000..087e1686ba --- /dev/null +++ b/benchmarks/resource_registry_bench.py @@ -0,0 +1,188 @@ +"""ASV benchmarks for resource registry lookup operations. + +Measures the performance of: +- ResourceTypeSpec construction from config dict +- ResourceRegistryService.show_type() lookup +- ResourceRegistryService.show_resource() lookup by name and ULID +- ResourceRegistryService.list_types() enumeration +- ResourceRegistryService.list_resources() enumeration +- ResourceRegistryService.register_resource() creation +""" + +from __future__ import annotations + +import sys +from pathlib import Path +from typing import Any + +try: + from cleveragents.application.services.resource_registry_service import ( + ResourceRegistryService, + ) + from cleveragents.domain.models.core.resource_type import ( + ResourceTypeSpec, + ) +except ModuleNotFoundError: + sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) + from cleveragents.application.services.resource_registry_service import ( + ResourceRegistryService, + ) + from cleveragents.domain.models.core.resource_type import ( + ResourceTypeSpec, + ) + + +def _setup_db() -> Any: + """Create in-memory database and return session factory.""" + from sqlalchemy import create_engine, event + from sqlalchemy.orm import sessionmaker + from sqlalchemy.pool import StaticPool + + from cleveragents.infrastructure.database.models import Base + + engine = create_engine( + "sqlite:///:memory:", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + + @event.listens_for(engine, "connect") + def _fk(conn: Any, _rec: Any) -> None: + conn.cursor().execute("PRAGMA foreign_keys=ON") + + Base.metadata.create_all(engine) + return sessionmaker(bind=engine) + + +class TypeSpecConstructionSuite: + """Benchmark ResourceTypeSpec construction from config dicts.""" + + def time_from_config_minimal(self) -> None: + """Benchmark minimal type spec construction.""" + ResourceTypeSpec.from_config( + { + "name": "bench/minimal", + "description": "Minimal benchmark type", + "resource_kind": "physical", + "sandbox_strategy": "copy_on_write", + } + ) + + def time_from_config_full(self) -> None: + """Benchmark full type spec construction with all fields.""" + ResourceTypeSpec.from_config( + { + "name": "bench/full", + "description": "Full benchmark type", + "resource_kind": "physical", + "sandbox_strategy": "git_worktree", + "user_addable": True, + "built_in": False, + "cli_args": [ + { + "name": "path", + "type": "path", + "required": True, + "description": "Path to resource", + }, + { + "name": "branch", + "type": "string", + "required": False, + "description": "Branch name", + "default": "main", + }, + ], + "parent_types": ["git-checkout"], + "child_types": ["fs-directory", "fs-file"], + "handler": "bench.handler:BenchHandler", + "capabilities": { + "read": True, + "write": True, + "sandbox": True, + "checkpoint": False, + }, + "auto_discovery": { + "enabled": True, + "rules": [ + {"type": "fs-directory", "pattern": "*/"}, + ], + }, + } + ) + + +class RegistryLookupSuite: + """Benchmark registry service lookup operations.""" + + def setup(self) -> None: + self._sf = _setup_db() + self._svc = ResourceRegistryService(session_factory=self._sf) + self._svc.bootstrap_builtin_types() + + # Register some resources for lookup benchmarks + self._resources = [] + for i in range(20): + r = self._svc.register_resource( + type_name="git-checkout", + name=f"bench/repo-{i}", + location=f"/tmp/bench/repo-{i}", + description=f"Benchmark repo {i}", + ) + self._resources.append(r) + + def time_show_type_builtin(self) -> None: + """Benchmark looking up a built-in type by name.""" + self._svc.show_type("git-checkout") + + def time_show_type_second_builtin(self) -> None: + """Benchmark looking up the second built-in type.""" + self._svc.show_type("fs-directory") + + def time_list_types(self) -> None: + """Benchmark listing all registered types.""" + self._svc.list_types() + + def time_list_types_filtered(self) -> None: + """Benchmark listing types filtered by namespace.""" + self._svc.list_types(namespace="builtin") + + def time_show_resource_by_name(self) -> None: + """Benchmark looking up a resource by namespaced name.""" + self._svc.show_resource("bench/repo-0") + + def time_show_resource_by_ulid(self) -> None: + """Benchmark looking up a resource by ULID.""" + self._svc.show_resource(self._resources[0].resource_id) + + def time_list_resources_all(self) -> None: + """Benchmark listing all resources.""" + self._svc.list_resources() + + def time_list_resources_filtered(self) -> None: + """Benchmark listing resources filtered by type.""" + self._svc.list_resources(type_name="git-checkout") + + +class RegistryRegistrationSuite: + """Benchmark resource registration throughput.""" + + _reg_ctr: int = 0 + + def setup(self) -> None: + self._sf = _setup_db() + self._svc = ResourceRegistryService(session_factory=self._sf) + self._svc.bootstrap_builtin_types() + + def time_register_resource(self) -> None: + """Benchmark registering a single resource.""" + RegistryRegistrationSuite._reg_ctr += 1 + self._svc.register_resource( + type_name="git-checkout", + name=f"bench/reg-{RegistryRegistrationSuite._reg_ctr}", + location=f"/tmp/bench/reg-{RegistryRegistrationSuite._reg_ctr}", + ) + + def time_bootstrap_builtin_types_idempotent(self) -> None: + """Benchmark idempotent bootstrap (types already exist).""" + self._svc.bootstrap_builtin_types() diff --git a/docs/reference/decision_model.md b/docs/reference/decision_model.md new file mode 100644 index 0000000000..2fa184d8d4 --- /dev/null +++ b/docs/reference/decision_model.md @@ -0,0 +1,122 @@ +# Decision Domain Model + +The decision subsystem records every choice point in a plan's lifecycle +as a **Decision** node in a persistent tree. Decisions are created +during the Strategize and Execute phases and form the basis for +targeted correction and replay. + +## Decision Types + +| Type | Phase | Description | +|--------------------------|------------|------------------------------------------| +| `prompt_definition` | Strategize | Root decision — the plan prompt | +| `invariant_enforced` | Strategize | An invariant constraint was applied | +| `strategy_choice` | Strategize | High-level approach chosen | +| `implementation_choice` | Execute | How to implement a specific task | +| `resource_selection` | Execute | Which resources to read / modify | +| `subplan_spawn` | Strategize | Decision to create a child plan | +| `subplan_parallel_spawn` | Strategize | Spawn a group of child plans in parallel | +| `tool_invocation` | Execute | Which skill / tool to use | +| `error_recovery` | Execute | How to handle a failure | +| `validation_response` | Execute | Response to a validation failure | +| `user_intervention` | Any | User-provided guidance / correction | + +## Decision Fields + +### Identity + +- **decision_id** — ULID, auto-generated +- **plan_id** — ULID of the parent plan (required) +- **parent_decision_id** — ULID of the parent decision, or `None` for the root +- **sequence_number** — monotonic order within the plan (0-indexed, never reused) + +### Classification + +- **decision_type** — one of the 11 `DecisionType` enum values + +### Content + +- **question** — what question was being answered (required, non-empty) +- **chosen_option** — the option that was selected (required, non-empty) +- **alternatives_considered** — list of other options evaluated +- **confidence_score** — float 0.0–1.0, or `None` if not applicable + +### Context Snapshot + +Every decision captures a `ContextSnapshot` for replay: + +- **hot_context_hash** — cryptographic hash of the context window +- **hot_context_ref** — storage pointer to the full serialised context +- **relevant_resources** — list of `ResourceRef` entries (resource_id + optional path) +- **actor_state_ref** — LangGraph actor checkpoint reference + +### Rationale + +- **rationale** — human-readable explanation +- **actor_reasoning** — raw LLM reasoning trace, if available + +### Downstream Impact + +- **downstream_decision_ids** — ULIDs of decisions that depend on this one +- **downstream_plan_ids** — ULIDs of child plans spawned from this decision +- **artifacts_produced** — list of `ArtifactRef` (artifact_path + artifact_type) + +### Timestamps + +- **created_at** — UTC datetime, auto-set on creation + +### Correction Metadata + +- **is_correction** — boolean, True if this decision corrects another +- **corrects_decision_id** — ULID of the original decision +- **correction_reason** — why the correction was made +- **superseded_by** — ULID of the decision that replaced this one + +## Tree Structure + +Decisions form a tree via `parent_decision_id`: + +``` +prompt_definition (root, parent=None) +├── invariant_enforced +├── strategy_choice +│ ├── implementation_choice +│ │ ├── resource_selection +│ │ └── tool_invocation +│ └── subplan_spawn +└── strategy_choice +``` + +The `prompt_definition` type is always the root and must have +`parent_decision_id = None`. This is enforced by a model validator. + +## Correction Lifecycle + +Corrections never mutate existing decisions. Instead: + +1. A new `Decision` is created with `is_correction=True` and + `corrects_decision_id` pointing to the original. +2. The original decision has its `superseded_by` field set to the + new decision's ID. +3. All downstream decisions of the original are also superseded. + +The **current tree** consists of all decisions where +`superseded_by IS NULL`. + +## Validation Rules + +- `plan_id` must be a valid ULID +- `parent_decision_id`, `corrects_decision_id`, `superseded_by` must + be valid ULIDs or None +- `confidence_score` must be in [0.0, 1.0] or None +- `prompt_definition` decisions must have `parent_decision_id = None` +- If `is_correction` is True, `corrects_decision_id` must be set +- If `corrects_decision_id` is set, `is_correction` must be True + +## Source + +- Domain model: `src/cleveragents/domain/models/core/decision.py` +- Specification: `docs/specification.md` L18390–L18521 +- ADR-007: Decision tree and correction +- ADR-033: Decision recording protocol +- ADR-034: Decision tree versioning and history diff --git a/docs/reference/resource_handlers.md b/docs/reference/resource_handlers.md new file mode 100644 index 0000000000..46d36bbd93 --- /dev/null +++ b/docs/reference/resource_handlers.md @@ -0,0 +1,172 @@ +# Resource Handlers + +Resource handlers bridge resource types to sandbox provisioning. When a plan +executes, each tool's resource slot is resolved to a physical sandbox path +through the handler pipeline. + +## Architecture + +``` +BindingResolutionService ResourceHandlerService + resolve(tool, project) resolve_binding(binding, plan_id) + │ │ + ▼ ▼ + BindingResult(resource_id) ┌── Resource (location) + │ ResourceTypeSpec (handler, strategy) + │ │ + │ ▼ + │ resolve_handler("module:Class") + │ │ + │ ▼ + │ handler.resolve(resource, sandbox_manager) + │ │ + │ ▼ + └── BoundResource(sandbox_path="/tmp/sandbox/...") +``` + +## Handler Protocol + +All handlers implement the `ResourceHandler` protocol: + +```python +class ResourceHandler(Protocol): + def resolve( + self, + *, + resource: Resource, + plan_id: str, + slot_name: str, + sandbox_manager: SandboxManager, + access: str = "read_only", + ) -> BoundResource: ... +``` + +## Built-in Handlers + +### GitCheckoutHandler + +Resolves `git-checkout` resources using the `git_worktree` sandbox strategy. + +| Property | Value | +|----------|-------| +| Module | `cleveragents.resource.handlers.git_checkout` | +| Class | `GitCheckoutHandler` | +| Default strategy | `git_worktree` | +| Fallback strategy | `copy_on_write` | +| Required fields | `resource.location` (path to git repo root) | + +### FsDirectoryHandler + +Resolves `fs-directory` resources using the `copy_on_write` sandbox strategy. + +| Property | Value | +|----------|-------| +| Module | `cleveragents.resource.handlers.fs_directory` | +| Class | `FsDirectoryHandler` | +| Default strategy | `copy_on_write` | +| Required fields | `resource.location` (path to directory) | + +## Handler Resolution + +Handler strings use the `module.path:ClassName` format and are stored on +`ResourceTypeSpec.handler`. Resolution is dynamic via `importlib`: + +```python +from cleveragents.resource.handlers import resolve_handler + +handler = resolve_handler( + "cleveragents.resource.handlers.git_checkout:GitCheckoutHandler" +) +``` + +Resolved handlers are cached for the process lifetime. Call +`clear_handler_cache()` to reset. + +### Fallback Behavior + +If no handler string is set (or resolution fails), the +`ResourceHandlerService` uses a default handler that delegates directly +to `SandboxManager` using the type's `sandbox_strategy`. + +## Strategy Precedence + +The sandbox strategy is determined in this order: + +1. **Resource-level override** (`resource.sandbox_strategy`) — per-resource +2. **Type default** (`ResourceTypeSpec.sandbox_strategy`) — per-type +3. **Fallback** (`none`) — no sandboxing + +## ResourceHandlerService + +The orchestration service that chains the resolution pipeline: + +```python +from cleveragents.application.services.resource_handler_service import ( + ResourceHandlerService, +) + +service = ResourceHandlerService( + sandbox_manager=sandbox_manager, + resource_lookup=registry_service.show_resource, + type_lookup=registry_service.show_type, +) + +# Resolve a single binding +bound = service.resolve_binding(binding, plan_id="01ARZ3...") + +# Resolve all bindings for a tool +bindings_map = service.resolve_bindings(bindings, plan_id="01ARZ3...") +# -> {"repo": BoundResource(sandbox_path="/tmp/sandbox/...")} +``` + +## Sandbox Outputs + +After resolution, each `BoundResource` carries: + +| Field | Description | +|-------|-------------| +| `slot_name` | Tool slot this binding fills | +| `resource_id` | ULID of the resolved resource | +| `resource_type` | Type name (e.g. `git-checkout`) | +| `sandbox_path` | Root path of the provisioned sandbox | +| `access` | `read_only` or `read_write` | + +The `sandbox_path` points to an isolated directory managed by +`SandboxManager`. Changes are committed or rolled back via +`SandboxManager.commit_all()` / `rollback_all()`. + +## Writing Custom Handlers + +To add a handler for a new resource type: + +1. Create a module under `cleveragents/resource/handlers/`. +2. Implement a class satisfying `ResourceHandler`. +3. Register it on the `ResourceTypeSpec` via the `handler` field: + `"cleveragents.resource.handlers.my_handler:MyHandler"`. + +```python +class MyHandler: + def resolve( + self, + *, + resource: Resource, + plan_id: str, + slot_name: str, + sandbox_manager: SandboxManager, + access: str = "read_only", + ) -> BoundResource: + # Custom validation / setup + sandbox = sandbox_manager.get_or_create_sandbox( + plan_id=plan_id, + resource_id=resource.resource_id, + original_path=resource.location, + sandbox_strategy="copy_on_write", + ) + return BoundResource( + slot_name=slot_name, + resource_id=resource.resource_id, + resource_type=resource.resource_type_name, + sandbox_path=sandbox.context.sandbox_path if sandbox.context else "", + access=access, + ) +``` diff --git a/docs/reference/resources.md b/docs/reference/resources.md new file mode 100644 index 0000000000..d8dbd7ce8d --- /dev/null +++ b/docs/reference/resources.md @@ -0,0 +1,211 @@ +# Resource Registry + +The Resource Registry manages **resource types** and **resource +instances** in CleverAgents. Resource types define schemas, sandbox +strategies, and handler implementations. Resource instances are +registered entries that plans operate on during execution. + +## Built-in Resource Types + +CleverAgents ships with two built-in types, available without +registration: + +| Type | Kind | Sandbox Strategy | Handler | +|------------------|----------|------------------|--------------------------------| +| `git-checkout` | physical | `git_worktree` | `GitCheckoutHandler` | +| `fs-directory` | physical | `copy_on_write` | `FsDirectoryHandler` | + +Built-in types are registered idempotently at startup via +`ResourceRegistryService.bootstrap_builtin_types()`. + +## Resource Type Fields + +Each `ResourceTypeSpec` defines a resource type schema: + +| Field | Type | Description | +|--------------------|---------------------|------------------------------------------------------| +| `name` | `str` | Unique name (built-in: bare, custom: `namespace/name`) | +| `description` | `str` | Human-readable description | +| `resource_kind` | `ResourceKind` | `physical` or `virtual` | +| `sandbox_strategy` | `SandboxStrategy` | Default sandbox isolation strategy | +| `user_addable` | `bool` | Whether users can register instances of this type | +| `built_in` | `bool` | Whether this is a built-in type | +| `cli_args` | `list[ResourceTypeArgument]` | CLI argument definitions for registration | +| `parent_types` | `list[str]` | Allowed parent type names in the DAG | +| `child_types` | `list[str]` | Allowed child type names in the DAG | +| `handler` | `str \| None` | Handler ref in `module:Class` format | +| `capabilities` | `dict[str, bool]` | `read`, `write`, `sandbox`, `checkpoint` flags | +| `auto_discovery` | `dict \| None` | Auto-discovery rules (see below) | +| `equivalence` | `dict \| None` | Virtual resource deduplication config | + +### Custom Types + +Custom types are defined in YAML and registered via CLI or API: + +```yaml +name: myteam/python-package +description: A Python package directory +resource_kind: physical +sandbox_strategy: copy_on_write +user_addable: true +cli_args: + - name: path + type: path + required: true + description: Path to the package root +parent_types: + - git-checkout +child_types: + - fs-directory +``` + +Register with: `agents resource type add path/to/type.yaml` + +## Resource Instance Fields + +Each `Resource` represents a registered asset: + +| Field | Type | Description | +|----------------------|-----------------------|----------------------------------------------| +| `resource_id` | `str` (ULID) | Unique identifier | +| `name` | `str \| None` | Optional namespaced name | +| `resource_type_name` | `str` | Type this resource belongs to | +| `classification` | `PhysVirt` | `physical` or `virtual` | +| `description` | `str \| None` | Human-readable description | +| `location` | `str \| None` | Filesystem path (physical resources) | +| `sandbox_strategy` | `SandboxStrategy \| None` | Per-resource sandbox override | +| `content_hash` | `str \| None` | Hash of resource contents for change detection | +| `parents` | `list[str]` | Parent resource IDs in the DAG | +| `children` | `list[str]` | Child resource IDs in the DAG | +| `properties` | `dict` | Type-specific key-value properties | +| `capabilities` | `ResourceCapabilities`| `readable`, `writable`, `sandboxable`, `checkpointable` | +| `created_at` | `datetime` | Registration timestamp | +| `updated_at` | `datetime` | Last modification timestamp | + +## Registry Service API + +### Type Operations + +| Method | Description | +|-------------------------|------------------------------------------| +| `bootstrap_builtin_types()` | Register built-in types (idempotent) | +| `register_type(config_path)` | Register a custom type from YAML | +| `list_types(namespace=)` | List types, optionally filtered | +| `show_type(name)` | Get a type by name | + +### Resource Operations + +| Method | Description | +|-------------------------------|--------------------------------------| +| `register_resource(type_name, name=, location=, ...)` | Create and register an instance | +| `list_resources(type_name=)` | List resources, optionally by type | +| `show_resource(name_or_id)` | Get by namespaced name or ULID | + +### DAG Operations + +| Method | Description | +|---------------------------------|------------------------------------| +| `link_child(parent, child)` | Link a child to a parent | +| `unlink_child(parent, child)` | Remove a parent-child link | +| `get_children(name_or_id)` | Get direct children | +| `get_resource_tree(name_or_id, depth=, type_filter=)` | Recursive tree traversal | + +For DAG rules, cycle detection, type compatibility, and auto-discovery +details, see [Resource DAG](resource_dag.md). + +## Discovery Behavior + +### Auto-Discovery + +Resource types can define auto-discovery rules that materialize child +resources automatically: + +```yaml +auto_discovery: + enabled: true + rules: + - type: fs-directory + pattern: "*/" + - type: fs-file + pattern: "*" +``` + +When `auto_discover_children(resource_id)` is called: + +1. The resource and its type are looked up. +2. Each discovery rule is evaluated: + - The child type must exist in the registry. + - The child type must be compatible with the parent type (per + `child_types`). +3. New child resources are created with `auto_discovered = true`. +4. Links are created in the `resource_links` table. + +Auto-discovery is triggered: +- When a resource is first registered +- When resource contents change +- On demand via CLI (`agents resource discover`) + +### Local-Mode Discovery + +In local mode (the default), discovery operates against the local +filesystem. The registry creates placeholder resource entries based +on discovery rules; actual file enumeration is delegated to resource +handlers at sandbox creation time. + +## Database Schema + +### `resource_types` Table + +| Column | Type | Description | +|---------------------------|---------------|--------------------------------| +| `name` | `String(128)` | Primary key | +| `namespace` | `String(64)` | Namespace (or `builtin`) | +| `description` | `Text` | Human-readable description | +| `resource_kind` | `String(16)` | `physical` or `virtual` | +| `sandbox_strategy` | `String(32)` | Default sandbox strategy | +| `user_addable` | `Boolean` | User registration allowed | +| `built_in` | `Boolean` | Built-in type flag | +| `handler` | `String(256)` | Handler `module:Class` ref | +| `args_schema_json` | `Text` | CLI args JSON | +| `allowed_parent_types_json` | `Text` | Allowed parent types JSON | +| `allowed_child_types_json` | `Text` | Allowed child types JSON | +| `auto_discover_json` | `Text` | Auto-discovery config JSON | +| `capabilities_json` | `Text` | Capabilities JSON | +| `equivalence_json` | `Text` | Equivalence config JSON | +| `source` | `String(256)` | Registration source | +| `created_at` | `String(30)` | ISO-8601 timestamp | +| `updated_at` | `String(30)` | ISO-8601 timestamp | + +### `resources` Table + +| Column | Type | Description | +|--------------------|---------------|-------------------------------------| +| `resource_id` | `String(26)` | ULID primary key | +| `namespaced_name` | `String(256)` | Optional namespaced name (unique) | +| `namespace` | `String(64)` | Namespace extracted from name | +| `type_name` | `String(128)` | FK to `resource_types.name` | +| `resource_kind` | `String(16)` | `physical` or `virtual` | +| `location` | `Text` | Filesystem path | +| `description` | `Text` | Human-readable description | +| `read_only` | `Boolean` | Read-only flag | +| `auto_discovered` | `Boolean` | Created by auto-discovery | +| `sandbox_strategy` | `String(32)` | Per-resource sandbox override | +| `content_hash` | `String(128)` | Content hash for change detection | +| `properties_json` | `Text` | Type-specific properties JSON | +| `metadata_json` | `Text` | Additional metadata JSON | +| `created_at` | `String(30)` | ISO-8601 timestamp | +| `updated_at` | `String(30)` | ISO-8601 timestamp | + +### `resource_links` Table + +See [Resource DAG](resource_dag.md#database-schema) for link table +schema. + +## Source + +- Domain models: `src/cleveragents/domain/models/core/resource.py`, + `resource_type.py` +- Service: `src/cleveragents/application/services/resource_registry_service.py` +- Migration: `alembic/versions/b1_001_resource_registry_tables.py` +- CLI: `src/cleveragents/cli/commands/resource.py` +- Specification: `docs/specification.md` — Resource Registry sections diff --git a/docs/specification.md b/docs/specification.md index ca5ef7e927..2f78a3d8f2 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -45250,11 +45250,11 @@ This section provides a high-level overview of the CleverAgents implementation r ### Current Status Summary -As of Day 14 (2026-02-22), the **Day 14 rebaseline plan** is authoritative and **88 of 182 COMMIT checklist items are complete** (~48.35%). The foundational layers — domain models, persistence, YAML schemas, CLI commands, quality automation, and CI/CD — are substantially built out. The primary blocker for the first end-to-end milestone remains the **tool-aware execute/apply pipeline**: the actor runtime that drives LLM-based plan execution, ChangeSet capture, and the sandbox merge path that commits changes to real resources with validation gating. +As of Day 15 (2026-02-23), the **Day 15 rebaseline plan** is authoritative and **1 of 53 rebaseline COMMIT checklist items are complete** (~1.89%). Foundational layers remain substantially built out (domain models, persistence, YAML schemas, CLI commands, quality automation, CI/CD, tool runtime, and diff review artifacts). The primary blocker for the first end-to-end milestone is now the **sandbox merge/apply path + resource handler bindings**: resource handlers must resolve git-checkout/fs-directory roots and the apply pipeline must merge sandbox changes into real resources with validation gating. -!!! warning "Critical Priority: v2 Feature Restoration" +!!! warning "Critical Priority: M1 Merge Path + Resource Handlers" - During the transition from the earlier v2 architecture to the current v3 spec-aligned design, approximately 4,000 lines of LangGraph agent infrastructure were stubbed or removed. **Restoring these v2 capabilities** — specifically the LangGraph-based execution actors, tool-calling runtime, and actor graph compilation — is the highest-priority blocker for reaching a minimal working version. Until the actor runtime can invoke tools through the LLM, persist decisions, and capture changes into a ChangeSet, the plan execute/apply lifecycle cannot function end-to-end. + The actor runtime and tool-aware execute flow are in place, but **apply is still blocked** by missing sandbox merge logic, validation gating, and resource handler bindings. Until git-checkout/fs-directory handlers can resolve sandbox roots and the apply pipeline can merge ChangeSets into real project resources safely, the plan lifecycle cannot complete end-to-end. ### Parallel Workstreams diff --git a/features/decision_model.feature b/features/decision_model.feature new file mode 100644 index 0000000000..76c8b6e2d2 --- /dev/null +++ b/features/decision_model.feature @@ -0,0 +1,246 @@ +Feature: Decision domain model + Validates the Decision, DecisionType, ContextSnapshot, ResourceRef, + and ArtifactRef domain models including ULID validation, enum coverage, + tree structure helpers, correction constraints, and serialization. + + # ------------------------------------------------------------------ + # DecisionType enum + # ------------------------------------------------------------------ + + Scenario: All 11 decision types are defined + Then the DecisionType enum should have exactly 11 members + + Scenario: Strategize-phase types are correctly classified + Then STRATEGIZE_TYPES should contain "prompt_definition" + And STRATEGIZE_TYPES should contain "invariant_enforced" + And STRATEGIZE_TYPES should contain "strategy_choice" + And STRATEGIZE_TYPES should contain "subplan_spawn" + And STRATEGIZE_TYPES should contain "subplan_parallel_spawn" + And STRATEGIZE_TYPES should have exactly 5 members + + Scenario: Execute-phase types are correctly classified + Then EXECUTE_TYPES should contain "implementation_choice" + And EXECUTE_TYPES should contain "resource_selection" + And EXECUTE_TYPES should contain "tool_invocation" + And EXECUTE_TYPES should contain "error_recovery" + And EXECUTE_TYPES should contain "validation_response" + And EXECUTE_TYPES should have exactly 5 members + + Scenario: user_intervention is not in either phase set + Then STRATEGIZE_TYPES should not contain "user_intervention" + And EXECUTE_TYPES should not contain "user_intervention" + + # ------------------------------------------------------------------ + # Decision creation and ULID validation + # ------------------------------------------------------------------ + + Scenario: Create a minimal root decision + Given a valid plan ULID + When I create a prompt_definition decision with sequence 0 + Then the decision should be created successfully + And the decision should be a root decision + And the decision should have a valid ULID as decision_id + And the decision type should be "prompt_definition" + And the decision is_strategize_type should be true + + Scenario: Create a non-root decision with parent + Given a valid plan ULID + And a valid parent decision ULID + When I create a strategy_choice decision with sequence 1 and a parent + Then the decision should be created successfully + And the decision should not be a root decision + And the decision is_strategize_type should be true + + Scenario: Create an execute-phase decision + Given a valid plan ULID + And a valid parent decision ULID + When I create an implementation_choice decision with sequence 2 and a parent + Then the decision should be created successfully + And the decision is_execute_type should be true + And the decision is_strategize_type should be false + + Scenario: user_intervention is valid in any phase + Given a valid plan ULID + When I create a user_intervention decision with sequence 3 + Then the decision should be created successfully + And the decision is_any_phase_type should be true + + Scenario: Invalid plan_id is rejected + When I try to create a decision with plan_id "not-a-ulid" + Then a decision validation error should be raised + And the decision error should mention "plan_id" + + Scenario: Invalid parent_decision_id is rejected + Given a valid plan ULID + When I try to create a decision with parent_decision_id "bad-id" + Then a decision validation error should be raised + And the decision error should mention "parent_decision_id" + + # ------------------------------------------------------------------ + # prompt_definition root constraint + # ------------------------------------------------------------------ + + Scenario: prompt_definition with a parent is rejected + Given a valid plan ULID + And a valid parent decision ULID + When I try to create a prompt_definition with a parent + Then a decision validation error should be raised + And the decision error should mention "root" + + # ------------------------------------------------------------------ + # Confidence score validation + # ------------------------------------------------------------------ + + Scenario: Confidence score within valid range + Given a valid plan ULID + When I create a decision with confidence score 0.85 + Then the decision should be created successfully + And the decision confidence score should be 0.85 + + Scenario: Confidence score of 0.0 is valid + Given a valid plan ULID + When I create a decision with confidence score 0.0 + Then the decision should be created successfully + + Scenario: Confidence score of 1.0 is valid + Given a valid plan ULID + When I create a decision with confidence score 1.0 + Then the decision should be created successfully + + Scenario: Confidence score above 1.0 is rejected + Given a valid plan ULID + When I try to create a decision with confidence score 1.5 + Then a decision validation error should be raised + + Scenario: Confidence score below 0.0 is rejected + Given a valid plan ULID + When I try to create a decision with confidence score -0.1 + Then a decision validation error should be raised + + Scenario: Confidence score of None is valid + Given a valid plan ULID + When I create a decision with confidence score None + Then the decision should be created successfully + And the decision confidence score should be None + + # ------------------------------------------------------------------ + # Correction metadata + # ------------------------------------------------------------------ + + Scenario: Create a correction decision + Given a valid plan ULID + And a valid corrected decision ULID + When I create a correction decision + Then the decision should be created successfully + And the decision is_correction should be true + + Scenario: is_correction without corrects_decision_id is rejected + Given a valid plan ULID + When I try to create a decision with is_correction true but no corrects_decision_id + Then a decision validation error should be raised + And the decision error should mention "corrects_decision_id" + + Scenario: corrects_decision_id without is_correction is rejected + Given a valid plan ULID + And a valid corrected decision ULID + When I try to create a decision with corrects_decision_id but is_correction false + Then a decision validation error should be raised + And the decision error should mention "is_correction" + + Scenario: superseded_by marks decision as superseded + Given a valid plan ULID + When I create a decision that is superseded + Then the decision is_superseded should be true + + Scenario: Invalid superseded_by ULID is rejected + Given a valid plan ULID + When I try to create a decision with superseded_by "invalid" + Then a decision validation error should be raised + And the decision error should mention "superseded_by" + + Scenario: Invalid corrects_decision_id ULID is rejected + Given a valid plan ULID + When I try to create a decision with corrects_decision_id ULID "not-valid" + Then a decision validation error should be raised + And the decision error should mention "corrects_decision_id" + + Scenario: with_superseded_by returns a new superseded copy + Given a valid plan ULID + When I create a prompt_definition decision with sequence 0 + And I call with_superseded_by on the decision + Then the original decision should not be superseded + And the superseded copy should be superseded + + # ------------------------------------------------------------------ + # ContextSnapshot + # ------------------------------------------------------------------ + + Scenario: Create a decision with a populated context snapshot + Given a valid plan ULID + And a context snapshot with hash and resources + When I create a decision with the context snapshot + Then the decision should be created successfully + And the decision context snapshot hash should not be empty + And the decision context snapshot should have resources + + Scenario: Default context snapshot is empty + Given a valid plan ULID + When I create a prompt_definition decision with sequence 0 + Then the decision context snapshot hash should be empty + + # ------------------------------------------------------------------ + # ResourceRef and ArtifactRef + # ------------------------------------------------------------------ + + Scenario: ResourceRef requires non-empty resource_id + When I try to create a ResourceRef with empty resource_id + Then a decision validation error should be raised + + Scenario: ArtifactRef requires non-empty artifact_path + When I try to create an ArtifactRef with empty artifact_path + Then a decision validation error should be raised + + Scenario: Decision with artifacts_produced + Given a valid plan ULID + When I create a decision with artifacts + Then the decision should have 2 artifacts produced + + # ------------------------------------------------------------------ + # Serialization + # ------------------------------------------------------------------ + + Scenario: Decision round-trips through dict serialization + Given a valid plan ULID + When I create a fully populated decision + Then the decision should round-trip through model_dump and model_validate + + Scenario: as_cli_dict returns expected keys + Given a valid plan ULID + When I create a prompt_definition decision with sequence 0 + Then as_cli_dict should contain key "decision_id" + And as_cli_dict should contain key "type" + And as_cli_dict should contain key "confidence" + And as_cli_dict should contain key "parent" + + # ------------------------------------------------------------------ + # All 11 decision types can be instantiated + # ------------------------------------------------------------------ + + Scenario Outline: Each decision type can be instantiated + Given a valid plan ULID + When I create a decision of type "" + Then the decision should be created successfully + + Examples: + | dtype | + | prompt_definition | + | invariant_enforced | + | strategy_choice | + | implementation_choice | + | resource_selection | + | subplan_spawn | + | subplan_parallel_spawn | + | tool_invocation | + | error_recovery | + | validation_response | + | user_intervention | diff --git a/features/resource_handlers.feature b/features/resource_handlers.feature new file mode 100644 index 0000000000..2f8dc2c481 --- /dev/null +++ b/features/resource_handlers.feature @@ -0,0 +1,124 @@ +Feature: Resource handler runtime + As a developer + I want resource handlers that resolve resources into sandbox-backed paths + So that tools receive isolated working directories during plan execution + + # === Handler Protocol === + + Scenario: GitCheckoutHandler satisfies ResourceHandler protocol + Then GitCheckoutHandler should satisfy the ResourceHandler protocol + + Scenario: FsDirectoryHandler satisfies ResourceHandler protocol + Then FsDirectoryHandler should satisfy the ResourceHandler protocol + + # === GitCheckoutHandler === + + Scenario: GitCheckoutHandler resolves a git-checkout resource + Given a git-checkout resource with location "/tmp/test-repo" + And a sandbox manager with a mock factory + When I resolve the resource with GitCheckoutHandler for plan "PLAN001" + Then the bound resource should have slot name "repo" + And the bound resource should have resource type "git-checkout" + And the bound resource sandbox path should not be empty + + Scenario: GitCheckoutHandler uses resource-level strategy override + Given a git-checkout resource at "/tmp/test-repo" using strategy "copy_on_write" + And a sandbox manager with a mock factory + When I resolve the resource with GitCheckoutHandler for plan "PLAN002" + Then the sandbox was created with strategy "copy_on_write" + + Scenario: GitCheckoutHandler rejects resource without location + Given a git-checkout resource without location + And a sandbox manager with a mock factory + When I try to resolve the resource with GitCheckoutHandler + Then a handler ValueError should be raised + And the handler error should mention "no location" + + # === FsDirectoryHandler === + + Scenario: FsDirectoryHandler resolves an fs-directory resource + Given an fs-directory resource with location "/tmp/test-dir" + And a sandbox manager with a mock factory + When I resolve the resource with FsDirectoryHandler for plan "PLAN003" + Then the bound resource should have slot name "workdir" + And the bound resource should have resource type "fs-directory" + And the bound resource sandbox path should not be empty + + Scenario: FsDirectoryHandler uses copy_on_write by default + Given an fs-directory resource with location "/tmp/test-dir" + And a sandbox manager with a mock factory + When I resolve the resource with FsDirectoryHandler for plan "PLAN004" + Then the sandbox was created with strategy "copy_on_write" + + Scenario: FsDirectoryHandler rejects resource without location + Given an fs-directory resource without location + And a sandbox manager with a mock factory + When I try to resolve the resource with FsDirectoryHandler + Then a handler ValueError should be raised + + # === Handler Resolver === + + Scenario: Resolve handler from valid module:class string + When I resolve handler "cleveragents.resource.handlers.git_checkout:GitCheckoutHandler" + Then the resolved handler should be a GitCheckoutHandler instance + + Scenario: Resolve handler from fs-directory reference + When I resolve handler "cleveragents.resource.handlers.fs_directory:FsDirectoryHandler" + Then the resolved handler should be a FsDirectoryHandler instance + + Scenario: Resolve handler caches instances + When I resolve handler "cleveragents.resource.handlers.git_checkout:GitCheckoutHandler" + And I resolve the same handler reference again + Then both resolved handlers should be the same object + + Scenario: Resolve handler rejects empty string + When I try to resolve an empty handler reference + Then a HandlerResolutionError should be raised + And the resolution error should mention "empty" + + Scenario: Resolve handler rejects missing colon separator + When I try to resolve handler "cleveragents.resource.handlers.git_checkout" + Then a HandlerResolutionError should be raised + And the resolution error should mention "module.path:ClassName" + + Scenario: Resolve handler rejects nonexistent module + When I try to resolve handler "nonexistent.module:SomeClass" + Then a HandlerResolutionError should be raised + And the resolution error should mention "Cannot import" + + Scenario: Resolve handler rejects nonexistent class in valid module + When I try to resolve handler "cleveragents.resource.handlers.git_checkout:NonExistentClass" + Then a HandlerResolutionError should be raised + And the resolution error should mention "not found" + + # === ResourceHandlerService === + + Scenario: ResourceHandlerService resolves a binding to BoundResource + Given a resource handler service with mock lookups + And a binding result for slot "repo" with resource id "RES001" + When I resolve the binding via resource handler service for plan "PLAN010" + Then the handler service should return a BoundResource + And the handler service BoundResource slot should be "repo" + And the handler service BoundResource sandbox path should not be empty + + Scenario: ResourceHandlerService skips deferred bindings + Given a resource handler service with mock lookups + And a deferred binding result for slot "extra" + And a binding result for slot "repo" with resource id "RES001" + When I resolve all bindings via resource handler service for plan "PLAN011" + Then the handler service should return 1 bound resource + And the handler service should have resolved slot "repo" + + Scenario: ResourceHandlerService rejects deferred single binding + Given a resource handler service with mock lookups + And a deferred binding result for slot "extra" + When I try to resolve the single deferred binding via resource handler service + Then a handler ValueError should be raised + And the handler error should mention "deferred" + + Scenario: ResourceHandlerService uses fallback handler when type has no handler string + Given a resource handler service with mock lookups and no handler string + And a binding result for slot "data" with resource id "RES002" + When I resolve the binding via resource handler service for plan "PLAN012" + Then the handler service should return a BoundResource + And the handler service BoundResource sandbox path should not be empty diff --git a/features/steps/decision_model_steps.py b/features/steps/decision_model_steps.py new file mode 100644 index 0000000000..ebc5087a49 --- /dev/null +++ b/features/steps/decision_model_steps.py @@ -0,0 +1,556 @@ +"""Step definitions for decision_model.feature. + +Tests the Decision, DecisionType, ContextSnapshot, ResourceRef, +and ArtifactRef domain models. +""" + +from __future__ import annotations + +from typing import Any + +from behave import given, then, when # type: ignore[import-untyped] +from behave.runner import Context +from pydantic import ValidationError +from ulid import ULID + +from cleveragents.domain.models.core.decision import ( + EXECUTE_TYPES, + STRATEGIZE_TYPES, + ArtifactRef, + ContextSnapshot, + Decision, + DecisionType, + ResourceRef, +) + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +_VALID_PLAN_ULID = str(ULID()) +_VALID_PARENT_ULID = str(ULID()) +_VALID_CORRECTED_ULID = str(ULID()) +_VALID_SUPERSEDED_ULID = str(ULID()) + + +def _make_decision( + plan_id: str = _VALID_PLAN_ULID, + parent_decision_id: str | None = None, + sequence_number: int = 0, + decision_type: DecisionType = DecisionType.PROMPT_DEFINITION, + question: str = "What approach should we take?", + chosen_option: str = "Build a REST API", + confidence_score: float | None = None, + context_snapshot: ContextSnapshot | None = None, + is_correction: bool = False, + corrects_decision_id: str | None = None, + correction_reason: str | None = None, + superseded_by: str | None = None, + artifacts_produced: list[ArtifactRef] | None = None, +) -> Decision: + kwargs: dict[str, Any] = { + "plan_id": plan_id, + "sequence_number": sequence_number, + "decision_type": decision_type, + "question": question, + "chosen_option": chosen_option, + "is_correction": is_correction, + } + if parent_decision_id is not None: + kwargs["parent_decision_id"] = parent_decision_id + if confidence_score is not None: + kwargs["confidence_score"] = confidence_score + if context_snapshot is not None: + kwargs["context_snapshot"] = context_snapshot + if corrects_decision_id is not None: + kwargs["corrects_decision_id"] = corrects_decision_id + if correction_reason is not None: + kwargs["correction_reason"] = correction_reason + if superseded_by is not None: + kwargs["superseded_by"] = superseded_by + if artifacts_produced is not None: + kwargs["artifacts_produced"] = artifacts_produced + return Decision(**kwargs) + + +# --------------------------------------------------------------------------- +# DecisionType enum +# --------------------------------------------------------------------------- + + +@then("the DecisionType enum should have exactly {count:d} members") +def step_decision_type_count(context: Context, count: int) -> None: + assert len(DecisionType) == count, ( + f"Expected {count} members, got {len(DecisionType)}" + ) + + +@then('STRATEGIZE_TYPES should contain "{dtype}"') +def step_strategize_contains(context: Context, dtype: str) -> None: + assert DecisionType(dtype) in STRATEGIZE_TYPES, f"{dtype} not in STRATEGIZE_TYPES" + + +@then('STRATEGIZE_TYPES should not contain "{dtype}"') +def step_strategize_not_contains(context: Context, dtype: str) -> None: + assert DecisionType(dtype) not in STRATEGIZE_TYPES + + +@then("STRATEGIZE_TYPES should have exactly {count:d} members") +def step_strategize_count(context: Context, count: int) -> None: + assert len(STRATEGIZE_TYPES) == count + + +@then('EXECUTE_TYPES should contain "{dtype}"') +def step_execute_contains(context: Context, dtype: str) -> None: + assert DecisionType(dtype) in EXECUTE_TYPES, f"{dtype} not in EXECUTE_TYPES" + + +@then('EXECUTE_TYPES should not contain "{dtype}"') +def step_execute_not_contains(context: Context, dtype: str) -> None: + assert DecisionType(dtype) not in EXECUTE_TYPES + + +@then("EXECUTE_TYPES should have exactly {count:d} members") +def step_execute_count(context: Context, count: int) -> None: + assert len(EXECUTE_TYPES) == count + + +# --------------------------------------------------------------------------- +# Given steps +# --------------------------------------------------------------------------- + + +@given("a valid plan ULID") +def step_valid_plan_ulid(context: Context) -> None: + context.decision_plan_id = str(ULID()) + + +@given("a valid parent decision ULID") +def step_valid_parent_ulid(context: Context) -> None: + context.decision_parent_id = str(ULID()) + + +@given("a valid corrected decision ULID") +def step_valid_corrected_ulid(context: Context) -> None: + context.decision_corrected_id = str(ULID()) + + +@given("a context snapshot with hash and resources") +def step_context_snapshot(context: Context) -> None: + context.decision_snapshot = ContextSnapshot( + hot_context_hash="sha256:abc123", + hot_context_ref="store://snapshots/abc123", + relevant_resources=[ + ResourceRef(resource_id=str(ULID()), path="src/main.py"), + ResourceRef(resource_id=str(ULID())), + ], + actor_state_ref="checkpoint://actor/001", + ) + + +# --------------------------------------------------------------------------- +# When steps — successful creation +# --------------------------------------------------------------------------- + + +@when("I create a prompt_definition decision with sequence {seq:d}") +def step_create_root(context: Context, seq: int) -> None: + context.decision_result = _make_decision( + plan_id=context.decision_plan_id, + sequence_number=seq, + decision_type=DecisionType.PROMPT_DEFINITION, + ) + + +@when("I create a strategy_choice decision with sequence {seq:d} and a parent") +def step_create_strategy(context: Context, seq: int) -> None: + context.decision_result = _make_decision( + plan_id=context.decision_plan_id, + parent_decision_id=context.decision_parent_id, + sequence_number=seq, + decision_type=DecisionType.STRATEGY_CHOICE, + ) + + +@when("I create an implementation_choice decision with sequence {seq:d} and a parent") +def step_create_impl(context: Context, seq: int) -> None: + context.decision_result = _make_decision( + plan_id=context.decision_plan_id, + parent_decision_id=context.decision_parent_id, + sequence_number=seq, + decision_type=DecisionType.IMPLEMENTATION_CHOICE, + ) + + +@when("I create a user_intervention decision with sequence {seq:d}") +def step_create_user_intervention(context: Context, seq: int) -> None: + context.decision_result = _make_decision( + plan_id=context.decision_plan_id, + sequence_number=seq, + decision_type=DecisionType.USER_INTERVENTION, + ) + + +@when("I create a decision with confidence score {score}") +def step_create_with_confidence(context: Context, score: str) -> None: + conf = None if score == "None" else float(score) + context.decision_result = _make_decision( + plan_id=context.decision_plan_id, + confidence_score=conf, + decision_type=DecisionType.STRATEGY_CHOICE, + ) + + +@when("I create a correction decision") +def step_create_correction(context: Context) -> None: + context.decision_result = _make_decision( + plan_id=context.decision_plan_id, + decision_type=DecisionType.STRATEGY_CHOICE, + is_correction=True, + corrects_decision_id=context.decision_corrected_id, + correction_reason="Original approach was suboptimal", + ) + + +@when("I create a decision that is superseded") +def step_create_superseded(context: Context) -> None: + context.decision_result = _make_decision( + plan_id=context.decision_plan_id, + decision_type=DecisionType.STRATEGY_CHOICE, + superseded_by=str(ULID()), + ) + + +@when("I create a decision with the context snapshot") +def step_create_with_snapshot(context: Context) -> None: + context.decision_result = _make_decision( + plan_id=context.decision_plan_id, + decision_type=DecisionType.STRATEGY_CHOICE, + context_snapshot=context.decision_snapshot, + ) + + +@when("I create a decision with artifacts") +def step_create_with_artifacts(context: Context) -> None: + context.decision_result = _make_decision( + plan_id=context.decision_plan_id, + decision_type=DecisionType.STRATEGY_CHOICE, + artifacts_produced=[ + ArtifactRef(artifact_path="src/api.py", artifact_type="file"), + ArtifactRef(artifact_path="patches/fix.patch", artifact_type="patch"), + ], + ) + + +@when("I create a fully populated decision") +def step_create_fully_populated(context: Context) -> None: + context.decision_result = _make_decision( + plan_id=context.decision_plan_id, + parent_decision_id=str(ULID()), + sequence_number=5, + decision_type=DecisionType.STRATEGY_CHOICE, + question="Which framework?", + chosen_option="FastAPI", + confidence_score=0.92, + context_snapshot=ContextSnapshot( + hot_context_hash="sha256:def456", + hot_context_ref="store://snapshots/def456", + relevant_resources=[ResourceRef(resource_id=str(ULID()))], + actor_state_ref="checkpoint://actor/005", + ), + artifacts_produced=[ArtifactRef(artifact_path="src/app.py")], + ) + + +@when('I create a decision of type "{dtype}"') +def step_create_any_type(context: Context, dtype: str) -> None: + dt = DecisionType(dtype) + parent = None if dt == DecisionType.PROMPT_DEFINITION else str(ULID()) + context.decision_result = _make_decision( + plan_id=context.decision_plan_id, + parent_decision_id=parent, + decision_type=dt, + ) + + +# --------------------------------------------------------------------------- +# When steps — expected failures +# --------------------------------------------------------------------------- + + +@when('I try to create a decision with plan_id "{plan_id}"') +def step_try_invalid_plan_id(context: Context, plan_id: str) -> None: + try: + _make_decision(plan_id=plan_id) + context.decision_error = None + except ValidationError as exc: + context.decision_error = exc + + +@when('I try to create a decision with parent_decision_id "{parent_id}"') +def step_try_invalid_parent(context: Context, parent_id: str) -> None: + try: + _make_decision( + plan_id=context.decision_plan_id, + parent_decision_id=parent_id, + decision_type=DecisionType.STRATEGY_CHOICE, + ) + context.decision_error = None + except ValidationError as exc: + context.decision_error = exc + + +@when("I try to create a prompt_definition with a parent") +def step_try_root_with_parent(context: Context) -> None: + try: + _make_decision( + plan_id=context.decision_plan_id, + parent_decision_id=context.decision_parent_id, + decision_type=DecisionType.PROMPT_DEFINITION, + ) + context.decision_error = None + except ValidationError as exc: + context.decision_error = exc + + +@when("I try to create a decision with confidence score {score}") +def step_try_bad_confidence(context: Context, score: str) -> None: + try: + _make_decision( + plan_id=context.decision_plan_id, + confidence_score=float(score), + decision_type=DecisionType.STRATEGY_CHOICE, + ) + context.decision_error = None + except ValidationError as exc: + context.decision_error = exc + + +@when("I try to create a decision with is_correction true but no corrects_decision_id") +def step_try_correction_no_target(context: Context) -> None: + try: + _make_decision( + plan_id=context.decision_plan_id, + decision_type=DecisionType.STRATEGY_CHOICE, + is_correction=True, + ) + context.decision_error = None + except ValidationError as exc: + context.decision_error = exc + + +@when("I try to create a decision with corrects_decision_id but is_correction false") +def step_try_correction_flag_mismatch(context: Context) -> None: + try: + _make_decision( + plan_id=context.decision_plan_id, + decision_type=DecisionType.STRATEGY_CHOICE, + is_correction=False, + corrects_decision_id=context.decision_corrected_id, + ) + context.decision_error = None + except ValidationError as exc: + context.decision_error = exc + + +@when('I try to create a decision with superseded_by "{value}"') +def step_try_bad_superseded(context: Context, value: str) -> None: + try: + _make_decision( + plan_id=context.decision_plan_id, + decision_type=DecisionType.STRATEGY_CHOICE, + superseded_by=value, + ) + context.decision_error = None + except ValidationError as exc: + context.decision_error = exc + + +@when('I try to create a decision with corrects_decision_id ULID "{value}"') +def step_try_bad_corrects_id(context: Context, value: str) -> None: + try: + _make_decision( + plan_id=context.decision_plan_id, + decision_type=DecisionType.STRATEGY_CHOICE, + is_correction=True, + corrects_decision_id=value, + ) + context.decision_error = None + except ValidationError as exc: + context.decision_error = exc + + +@when("I call with_superseded_by on the decision") +def step_call_with_superseded_by(context: Context) -> None: + context.decision_superseded_copy = context.decision_result.with_superseded_by( + str(ULID()) + ) + + +@when("I try to create a ResourceRef with empty resource_id") +def step_try_empty_resource_ref(context: Context) -> None: + try: + ResourceRef(resource_id="") + context.decision_error = None + except ValidationError as exc: + context.decision_error = exc + + +@when("I try to create an ArtifactRef with empty artifact_path") +def step_try_empty_artifact_ref(context: Context) -> None: + try: + ArtifactRef(artifact_path="") + context.decision_error = None + except ValidationError as exc: + context.decision_error = exc + + +# --------------------------------------------------------------------------- +# Then steps +# --------------------------------------------------------------------------- + + +@then("the decision should be created successfully") +def step_decision_created(context: Context) -> None: + assert context.decision_result is not None + assert isinstance(context.decision_result, Decision) + + +@then("the decision should be a root decision") +def step_is_root(context: Context) -> None: + assert context.decision_result.is_root + + +@then("the decision should not be a root decision") +def step_not_root(context: Context) -> None: + assert not context.decision_result.is_root + + +@then("the decision should have a valid ULID as decision_id") +def step_valid_decision_id(context: Context) -> None: + import re + + pattern = r"^[0-9A-HJKMNP-TV-Z]{26}$" + assert re.match(pattern, context.decision_result.decision_id), ( + f"Invalid ULID: {context.decision_result.decision_id}" + ) + + +@then('the decision type should be "{dtype}"') +def step_decision_type_check(context: Context, dtype: str) -> None: + assert context.decision_result.decision_type == DecisionType(dtype) + + +@then("the decision is_strategize_type should be true") +def step_is_strategize(context: Context) -> None: + assert context.decision_result.is_strategize_type + + +@then("the decision is_strategize_type should be false") +def step_not_strategize(context: Context) -> None: + assert not context.decision_result.is_strategize_type + + +@then("the decision is_execute_type should be true") +def step_is_execute(context: Context) -> None: + assert context.decision_result.is_execute_type + + +@then("the decision is_execute_type should be false") +def step_not_execute(context: Context) -> None: + assert not context.decision_result.is_execute_type + + +@then("the decision is_any_phase_type should be true") +def step_is_any_phase(context: Context) -> None: + assert context.decision_result.is_any_phase_type + + +@then("the decision confidence score should be {score}") +def step_confidence_value(context: Context, score: str) -> None: + if score == "None": + assert context.decision_result.confidence_score is None + else: + assert context.decision_result.confidence_score == float(score) + + +@then("the decision is_correction should be true") +def step_is_correction(context: Context) -> None: + assert context.decision_result.is_correction + + +@then("the decision is_superseded should be true") +def step_is_superseded(context: Context) -> None: + assert context.decision_result.is_superseded + + +@then("the original decision should not be superseded") +def step_original_not_superseded(context: Context) -> None: + assert not context.decision_result.is_superseded + + +@then("the superseded copy should be superseded") +def step_copy_is_superseded(context: Context) -> None: + assert context.decision_superseded_copy.is_superseded + assert context.decision_superseded_copy.decision_id == ( + context.decision_result.decision_id + ) + + +@then("a decision validation error should be raised") +def step_validation_error(context: Context) -> None: + assert context.decision_error is not None, ( + "Expected ValidationError but none raised" + ) + assert isinstance(context.decision_error, ValidationError) + + +@then('the decision error should mention "{text}"') +def step_error_mentions(context: Context, text: str) -> None: + assert text.lower() in str(context.decision_error).lower(), ( + f"Expected '{text}' in error: {context.decision_error}" + ) + + +@then("the decision context snapshot hash should not be empty") +def step_snapshot_hash_not_empty(context: Context) -> None: + assert context.decision_result.context_snapshot.hot_context_hash + + +@then("the decision context snapshot should have resources") +def step_snapshot_has_resources(context: Context) -> None: + assert len(context.decision_result.context_snapshot.relevant_resources) > 0 + + +@then("the decision context snapshot hash should be empty") +def step_snapshot_hash_empty(context: Context) -> None: + assert context.decision_result.context_snapshot.hot_context_hash == "" + + +@then("the decision should have {count:d} artifacts produced") +def step_artifact_count(context: Context, count: int) -> None: + assert len(context.decision_result.artifacts_produced) == count + + +@then("the decision should round-trip through model_dump and model_validate") +def step_roundtrip(context: Context) -> None: + data = context.decision_result.model_dump() + restored = Decision.model_validate(data) + assert restored.decision_id == context.decision_result.decision_id + assert restored.decision_type == context.decision_result.decision_type + assert restored.question == context.decision_result.question + assert restored.chosen_option == context.decision_result.chosen_option + assert restored.confidence_score == context.decision_result.confidence_score + assert ( + restored.context_snapshot.hot_context_hash + == context.decision_result.context_snapshot.hot_context_hash + ) + assert len(restored.artifacts_produced) == len( + context.decision_result.artifacts_produced + ) + + +@then('as_cli_dict should contain key "{key}"') +def step_cli_dict_key(context: Context, key: str) -> None: + cli = context.decision_result.as_cli_dict() + assert key in cli, f"Key '{key}' not in as_cli_dict: {list(cli.keys())}" diff --git a/features/steps/resource_handlers_steps.py b/features/steps/resource_handlers_steps.py new file mode 100644 index 0000000000..c9e87db712 --- /dev/null +++ b/features/steps/resource_handlers_steps.py @@ -0,0 +1,462 @@ +"""Step definitions for resource_handlers.feature. + +Tests the resource handler protocol, GitCheckoutHandler, FsDirectoryHandler, +handler resolver, and ResourceHandlerService orchestration bridge. +""" + +from __future__ import annotations + +from datetime import datetime +from unittest.mock import MagicMock, PropertyMock + +from behave import given, then, when # type: ignore[import-untyped] +from behave.runner import Context + +from cleveragents.domain.models.core.resource import ( + PhysVirt, + Resource, + ResourceCapabilities, + SandboxStrategy, +) +from cleveragents.domain.models.core.resource_slot import BindingResult +from cleveragents.domain.models.core.resource_type import ( + ResourceKind, + ResourceTypeSpec, +) +from cleveragents.domain.models.core.resource_type import ( + SandboxStrategy as TypeSandboxStrategy, +) +from cleveragents.infrastructure.sandbox.factory import SandboxFactory +from cleveragents.infrastructure.sandbox.manager import SandboxManager +from cleveragents.infrastructure.sandbox.protocol import ( + SandboxContext, + SandboxStatus, +) +from cleveragents.resource.handlers.fs_directory import FsDirectoryHandler +from cleveragents.resource.handlers.git_checkout import GitCheckoutHandler +from cleveragents.resource.handlers.protocol import ResourceHandler +from cleveragents.resource.handlers.resolver import ( + HandlerResolutionError, + clear_handler_cache, + resolve_handler, +) +from cleveragents.tool.context import BoundResource + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_resource( + resource_type_name: str, + location: str | None = None, + sandbox_strategy: SandboxStrategy | None = None, + resource_id: str = "01KJ5C5TPMP8GGX3QC83E2MAQS", +) -> Resource: + """Create a test Resource domain object.""" + return Resource( + resource_id=resource_id, + resource_type_name=resource_type_name, + classification=PhysVirt.PHYSICAL, + location=location, + sandbox_strategy=sandbox_strategy, + capabilities=ResourceCapabilities( + readable=True, writable=True, sandboxable=True + ), + ) + + +def _make_mock_sandbox_manager() -> tuple[SandboxManager, MagicMock]: + """Create a SandboxManager with a mock factory that tracks calls.""" + mock_factory = MagicMock(spec=SandboxFactory) + + mock_sandbox = MagicMock() + mock_sandbox.sandbox_id = "sb-test-001" + type(mock_sandbox).status = PropertyMock(return_value=SandboxStatus.CREATED) + mock_sandbox.context = SandboxContext( + sandbox_id="sb-test-001", + sandbox_path="/tmp/sandbox/sb-test-001", + original_path="/tmp/original", + resource_id="res-test", + plan_id="plan-test", + created_at=datetime.now(), + ) + mock_sandbox.create.return_value = mock_sandbox.context + + mock_factory.create_sandbox.return_value = mock_sandbox + + manager = SandboxManager(factory=mock_factory, cleanup_on_exit=False) + return manager, mock_factory + + +def _make_type_spec( + name: str = "git-checkout", + sandbox_strategy: str = "git_worktree", + handler: str + | None = "cleveragents.resource.handlers.git_checkout:GitCheckoutHandler", +) -> ResourceTypeSpec: + """Create a minimal ResourceTypeSpec for testing.""" + return ResourceTypeSpec( + name=name, + description=f"Test {name} type", + resource_kind=ResourceKind.PHYSICAL, + sandbox_strategy=TypeSandboxStrategy(sandbox_strategy), + user_addable=True, + built_in=True, + handler=handler, + ) + + +# --------------------------------------------------------------------------- +# Protocol conformance +# --------------------------------------------------------------------------- + + +@then("GitCheckoutHandler should satisfy the ResourceHandler protocol") +def step_git_handler_protocol(context: Context) -> None: + handler = GitCheckoutHandler() + assert isinstance(handler, ResourceHandler), ( + "GitCheckoutHandler does not satisfy ResourceHandler protocol" + ) + + +@then("FsDirectoryHandler should satisfy the ResourceHandler protocol") +def step_fs_handler_protocol(context: Context) -> None: + handler = FsDirectoryHandler() + assert isinstance(handler, ResourceHandler), ( + "FsDirectoryHandler does not satisfy ResourceHandler protocol" + ) + + +# --------------------------------------------------------------------------- +# GitCheckoutHandler +# --------------------------------------------------------------------------- + + +@given('a git-checkout resource with location "{location}"') +def step_git_resource(context: Context, location: str) -> None: + context.handler_resource = _make_resource("git-checkout", location=location) + + +@given('a git-checkout resource at "{location}" using strategy "{strategy}"') +def step_git_resource_with_strategy( + context: Context, location: str, strategy: str +) -> None: + context.handler_resource = _make_resource( + "git-checkout", + location=location, + sandbox_strategy=SandboxStrategy(strategy), + ) + + +@given("a git-checkout resource without location") +def step_git_resource_no_location(context: Context) -> None: + context.handler_resource = _make_resource("git-checkout", location=None) + + +@given("a sandbox manager with a mock factory") +def step_mock_sandbox_manager(context: Context) -> None: + context.handler_sandbox_manager, context.handler_mock_factory = ( + _make_mock_sandbox_manager() + ) + + +@when('I resolve the resource with GitCheckoutHandler for plan "{plan_id}"') +def step_resolve_git(context: Context, plan_id: str) -> None: + handler = GitCheckoutHandler() + context.handler_bound = handler.resolve( + resource=context.handler_resource, + plan_id=plan_id, + slot_name="repo", + sandbox_manager=context.handler_sandbox_manager, + ) + + +@when("I try to resolve the resource with GitCheckoutHandler") +def step_try_resolve_git(context: Context) -> None: + handler = GitCheckoutHandler() + try: + handler.resolve( + resource=context.handler_resource, + plan_id="PLAN_ERR", + slot_name="repo", + sandbox_manager=context.handler_sandbox_manager, + ) + context.handler_error = None + except ValueError as exc: + context.handler_error = exc + + +# --------------------------------------------------------------------------- +# FsDirectoryHandler +# --------------------------------------------------------------------------- + + +@given('an fs-directory resource with location "{location}"') +def step_fs_resource(context: Context, location: str) -> None: + context.handler_resource = _make_resource("fs-directory", location=location) + + +@given("an fs-directory resource without location") +def step_fs_resource_no_location(context: Context) -> None: + context.handler_resource = _make_resource("fs-directory", location=None) + + +@when('I resolve the resource with FsDirectoryHandler for plan "{plan_id}"') +def step_resolve_fs(context: Context, plan_id: str) -> None: + handler = FsDirectoryHandler() + context.handler_bound = handler.resolve( + resource=context.handler_resource, + plan_id=plan_id, + slot_name="workdir", + sandbox_manager=context.handler_sandbox_manager, + ) + + +@when("I try to resolve the resource with FsDirectoryHandler") +def step_try_resolve_fs(context: Context) -> None: + handler = FsDirectoryHandler() + try: + handler.resolve( + resource=context.handler_resource, + plan_id="PLAN_ERR", + slot_name="workdir", + sandbox_manager=context.handler_sandbox_manager, + ) + context.handler_error = None + except ValueError as exc: + context.handler_error = exc + + +# --------------------------------------------------------------------------- +# Shared handler assertions +# --------------------------------------------------------------------------- + + +@then('the bound resource should have slot name "{name}"') +def step_bound_slot(context: Context, name: str) -> None: + assert context.handler_bound.slot_name == name + + +@then('the bound resource should have resource type "{rtype}"') +def step_bound_type(context: Context, rtype: str) -> None: + assert context.handler_bound.resource_type == rtype + + +@then("the bound resource sandbox path should not be empty") +def step_bound_sandbox_not_empty(context: Context) -> None: + assert context.handler_bound.sandbox_path, ( + f"sandbox_path is empty: {context.handler_bound.sandbox_path!r}" + ) + + +@then('the sandbox was created with strategy "{strategy}"') +def step_sandbox_strategy(context: Context, strategy: str) -> None: + call_args = context.handler_mock_factory.create_sandbox.call_args + assert call_args is not None, "create_sandbox was not called" + assert call_args.kwargs.get("sandbox_strategy") == strategy or ( + len(call_args.args) >= 3 and call_args.args[2] == strategy + ), f"Expected strategy {strategy}, got {call_args}" + + +@then("a handler ValueError should be raised") +def step_handler_value_error(context: Context) -> None: + assert context.handler_error is not None + assert isinstance(context.handler_error, ValueError) + + +@then('the handler error should mention "{text}"') +def step_handler_error_text(context: Context, text: str) -> None: + assert text.lower() in str(context.handler_error).lower(), ( + f"Expected '{text}' in error: {context.handler_error}" + ) + + +# --------------------------------------------------------------------------- +# Handler Resolver +# --------------------------------------------------------------------------- + + +@when('I resolve handler "{ref}"') +def step_resolve_handler(context: Context, ref: str) -> None: + clear_handler_cache() + context.handler_resolved = resolve_handler(ref) + context.handler_ref_used = ref + + +@when("I resolve the same handler reference again") +def step_resolve_handler_again(context: Context) -> None: + context.handler_resolved_second = resolve_handler(context.handler_ref_used) + + +@then("the resolved handler should be a GitCheckoutHandler instance") +def step_resolved_is_git(context: Context) -> None: + assert isinstance(context.handler_resolved, GitCheckoutHandler) + + +@then("the resolved handler should be a FsDirectoryHandler instance") +def step_resolved_is_fs(context: Context) -> None: + assert isinstance(context.handler_resolved, FsDirectoryHandler) + + +@then("both resolved handlers should be the same object") +def step_same_object(context: Context) -> None: + assert context.handler_resolved is context.handler_resolved_second + + +@when("I try to resolve an empty handler reference") +def step_try_resolve_empty_handler(context: Context) -> None: + clear_handler_cache() + try: + resolve_handler("") + context.handler_resolution_error = None + except HandlerResolutionError as exc: + context.handler_resolution_error = exc + + +@when('I try to resolve handler "{ref}"') +def step_try_resolve_handler(context: Context, ref: str) -> None: + clear_handler_cache() + try: + resolve_handler(ref) + context.handler_resolution_error = None + except HandlerResolutionError as exc: + context.handler_resolution_error = exc + + +@then("a HandlerResolutionError should be raised") +def step_resolution_error(context: Context) -> None: + assert context.handler_resolution_error is not None + assert isinstance(context.handler_resolution_error, HandlerResolutionError) + + +@then('the resolution error should mention "{text}"') +def step_resolution_error_text(context: Context, text: str) -> None: + assert text.lower() in str(context.handler_resolution_error).lower(), ( + f"Expected '{text}' in error: {context.handler_resolution_error}" + ) + + +# --------------------------------------------------------------------------- +# ResourceHandlerService +# --------------------------------------------------------------------------- + + +@given("a resource handler service with mock lookups") +def step_handler_service(context: Context) -> None: + from cleveragents.application.services.resource_handler_service import ( + ResourceHandlerService, + ) + + manager, _mock_factory = _make_mock_sandbox_manager() + resource = _make_resource("git-checkout", location="/tmp/test-repo") + type_spec = _make_type_spec() + + context.handler_svc = ResourceHandlerService( + sandbox_manager=manager, + resource_lookup=lambda _name_or_id: resource, + type_lookup=lambda _name: type_spec, + ) + context.handler_svc_manager = manager + + +@given("a resource handler service with mock lookups and no handler string") +def step_handler_service_no_handler(context: Context) -> None: + from cleveragents.application.services.resource_handler_service import ( + ResourceHandlerService, + ) + + manager, _mock_factory = _make_mock_sandbox_manager() + resource = _make_resource("fs-directory", location="/tmp/test-dir") + type_spec = _make_type_spec( + name="fs-directory", + sandbox_strategy="copy_on_write", + handler=None, + ) + + context.handler_svc = ResourceHandlerService( + sandbox_manager=manager, + resource_lookup=lambda _name_or_id: resource, + type_lookup=lambda _name: type_spec, + ) + + +@given('a binding result for slot "{slot}" with resource id "{res_id}"') +def step_binding_result(context: Context, slot: str, res_id: str) -> None: + if not hasattr(context, "handler_bindings"): + context.handler_bindings = [] + context.handler_binding = BindingResult( + slot_name=slot, + resource_id=res_id, + resource_name=f"test-{res_id}", + binding_mode="contextual", + ) + context.handler_bindings.append(context.handler_binding) + + +@given('a deferred binding result for slot "{slot}"') +def step_deferred_binding(context: Context, slot: str) -> None: + if not hasattr(context, "handler_bindings"): + context.handler_bindings = [] + context.handler_deferred_binding = BindingResult( + slot_name=slot, + resource_id=None, + binding_mode="parameter", + deferred=True, + ) + context.handler_bindings.append(context.handler_deferred_binding) + + +@when('I resolve the binding via resource handler service for plan "{plan_id}"') +def step_svc_resolve_binding(context: Context, plan_id: str) -> None: + context.handler_svc_bound = context.handler_svc.resolve_binding( + binding=context.handler_binding, + plan_id=plan_id, + ) + + +@when('I resolve all bindings via resource handler service for plan "{plan_id}"') +def step_svc_resolve_all(context: Context, plan_id: str) -> None: + context.handler_svc_all = context.handler_svc.resolve_bindings( + bindings=context.handler_bindings, + plan_id=plan_id, + ) + + +@when("I try to resolve the single deferred binding via resource handler service") +def step_svc_try_resolve_deferred(context: Context) -> None: + try: + context.handler_svc.resolve_binding( + binding=context.handler_deferred_binding, + plan_id="PLAN_ERR", + ) + context.handler_error = None + except ValueError as exc: + context.handler_error = exc + + +@then("the handler service should return a BoundResource") +def step_svc_is_bound(context: Context) -> None: + assert isinstance(context.handler_svc_bound, BoundResource) + + +@then('the handler service BoundResource slot should be "{slot}"') +def step_svc_bound_slot(context: Context, slot: str) -> None: + assert context.handler_svc_bound.slot_name == slot + + +@then("the handler service BoundResource sandbox path should not be empty") +def step_svc_bound_path(context: Context) -> None: + assert context.handler_svc_bound.sandbox_path, ( + f"sandbox_path is empty: {context.handler_svc_bound.sandbox_path!r}" + ) + + +@then("the handler service should return {count:d} bound resource") +def step_svc_count(context: Context, count: int) -> None: + assert len(context.handler_svc_all) == count + + +@then('the handler service should have resolved slot "{slot}"') +def step_svc_has_slot(context: Context, slot: str) -> None: + assert slot in context.handler_svc_all diff --git a/implementation_plan.md b/implementation_plan.md index 8588e2675b..530d0ed09c 100644 --- a/implementation_plan.md +++ b/implementation_plan.md @@ -2167,6 +2167,46 @@ Each schedule update entry must include: timeline reference, summary, milestone | M6+ | 0/1 | 0/0 | 0/7 | 0/7 | 0/1 | 0/1 | 0/17 | | Total | 43/62 | 8/14 | 8/29 | 2/24 | 13/21 | 0/1 | 74/151 | +### 2026-02-23 (Day 15 since kickoff on 2026-02-09) +- Timeline reference: Day 7/M1 = 2026-02-15, Day 14/M2 = 2026-02-22, Day 18/M3 = 2026-02-26, Day 22/M4 = 2026-03-02, Day 26/M5 = 2026-03-06, Day 30/M6 = 2026-03-10. +- Summary (Team | Status | Risk | Notes): Behind ~8-10d | HIGH | D0b.execute/apply landed, but M1 still blocked by sandbox apply merge + resource handler binding; M2 actor/tool sources queued; Rui out until 2026-02-27 keeps QA risk elevated. +- Milestone forecast (Target -> ETA | Delta | Risk): + - M1 (2026-02-15) -> ETA 2026-02-26 | +11d | HIGH + - M2 (2026-02-22) -> ETA 2026-03-03 | +9d | HIGH + - M3 (2026-02-26) -> ETA 2026-03-07 | +9d | MED-HIGH + - M4 (2026-03-02) -> ETA 2026-03-11 | +9d | MED-HIGH + - M5 (2026-03-06) -> ETA 2026-03-13 | +7d | MEDIUM + - M6 (2026-03-10) -> ETA 2026-03-15 | +5d | MEDIUM +- Track forecast (Track | Status | ETA | Delta | Risk | Blocking): + - Track A (Tool-aware execute + apply) | behind ~8-10d | ETA 2026-02-26 | +11d | HIGH | sandbox merge/apply + resource handler binding still pending + - Track B (Resources + sandbox handlers) | behind ~5-6d | ETA 2026-02-26 | +11d | MED-HIGH | handler runtime + DAG link/unlink CLI + binding resolution + - Track C (Actors/tools/skills/MCP) | behind ~7-8d | ETA 2026-03-03 | +9d | HIGH | hierarchical actor YAML + compiler + MCP + Agent Skills loader + - Track D (Decisions/validations/invariants) | behind ~9-10d | ETA 2026-03-07 | +9d | HIGH | decision persistence + validation runner + correction flows + - Track E (Corrections/subplans/checkpoints) | behind ~8-10d | ETA 2026-03-11 | +9d | MED-HIGH | subplan orchestration + merge + rollback hooks + - Track F (ACMS v1/context scaling) | behind ~7-9d | ETA 2026-03-13 | +7d | MEDIUM | UKO + CRP + context pipeline + - Track Q (Quality automation) | ahead | ETA complete | LOW | maintenance only + - Track T (Testing) | at risk (Rui out) | ETA 2026-03-07+ | MED-HIGH | QA load on Brent + feature owners +- Developer forecast (Name | Days Ahead/Behind | Availability | Risk | Focus): + - Jeff | -2d behind | available | HIGH | finish M1 apply path, unblock M2 actor compiler and Agent Skills registry + - Luis | -3d behind | available | HIGH | apply pipeline + validation runner + diff persistence + - Hamza | -1d behind | available | MED-HIGH | resource handlers + bindings + decision domain groundwork + - Aditya | +1d ahead | available | MEDIUM | hierarchical actor YAML + MCP adapter + Agent Skills loader + - Brent | on track | available | MED-HIGH | M1/M2 smoke suites + coverage enforcement + - Rui | N/A (unavailable) | unavailable 2026-02-13 to 2026-02-27 | HIGH | integration suites after return + - Mike/Brian | N/A | standby | LOW | none +- Task inventory (Milestone × Developer, current rebaseline): + Note: Each cell is **completed/total COMMIT items** for that milestone/owner using the Day 15 rebaseline below. + | Milestone | Jeff | Aditya | Luis | Hamza | Brent | Rui | Total | + |-----------|------|--------|------|-------|-------|-----|-------| + | M1 | 1/3 | 0/0 | 0/1 | 0/1 | 0/1 | 0/0 | 1/6 | + | M2 | 0/2 | 0/4 | 0/1 | 0/1 | 0/1 | 0/0 | 0/9 | + | M3 | 0/1 | 0/0 | 0/2 | 0/3 | 0/2 | 0/0 | 0/8 | + | M4 | 0/1 | 0/0 | 0/2 | 0/1 | 0/1 | 0/0 | 0/5 | + | M5 | 0/2 | 0/1 | 0/2 | 0/3 | 0/1 | 0/0 | 0/9 | + | M6 | 0/2 | 0/0 | 0/3 | 0/2 | 0/1 | 0/0 | 0/8 | + | M6+ | 0/1 | 0/0 | 0/3 | 0/3 | 0/1 | 0/1 | 0/9 | + | Total | 1/12 | 0/5 | 0/14 | 0/14 | 0/7 | 0/1 | 1/53 | + ### A9.service: Config Service with Multi-Level Resolution @@ -2218,16 +2258,16 @@ This comprehensive checklist tracks all implementation tasks for the CleverAgent **Organization**: This checklist is organized to enable parallel development and achieve a minimally working version as quickly as possible. Workstreams are clearly marked. Dependencies between workstreams are noted at merge points. -> **CRITICAL PRIORITY DIRECTIVE (as of Day 14, 2026-02-22)**: -> The **M1 source-code MVP** is blocked by tool-aware execution and sandbox apply. The following tasks are the highest priority items across the entire project: +> **CRITICAL PRIORITY DIRECTIVE (as of Day 15, 2026-02-23)**: +> The **M1 source-code MVP** is blocked by resource handlers + sandbox apply merge + validation gating. The following tasks are the highest priority items across the entire project: > -> 1. **M1.1** (Jeff) — Tool-calling actor runtime (execution actors) -> 2. **M1.2** (Jeff) — Plan execute wiring + ChangeSet capture via tool runtime -> 3. **M1.3** (Luis) — Sandbox merge/apply pipeline with validation gating hooks -> 4. **M1.4** (Hamza) — Resource handler runtime + tool/resource bindings -> 5. **M1.5** (Brent) — M1 end-to-end source-code smoke suite +> 1. **M1.3** (Luis) — Sandbox merge/apply pipeline with validation gating hooks +> 2. **M1.4** (Hamza) — Resource handler runtime + tool/resource bindings +> 3. **M1.5** (Brent) — M1 end-to-end source-code smoke suite +> 4. **M2.1** (Aditya) — Hierarchical actor YAML schema + examples +> 5. **M2.2** (Jeff) — Actor compiler + runtime wiring > -> **Jeff MUST finish the actor runtime + plan execute wiring before M2.** +> **Jeff MUST finish the actor compiler + runtime wiring before M2.** > > **Aditya**: Prioritize hierarchical actor YAML configs + MCP/Agent Skills loaders (M2). > @@ -2250,14 +2290,14 @@ Execute all required tests through the appropriate `nox` sessions—never call ` - **PR requirement**: every branch with one or more commits must have a Forgejo PR task with an explicit description; merge happens in the Forgejo UI after CI + review. - **Compatibility note**: If any existing subtrack still lists CLI merges (e.g., `git checkout master && git merge --no-ff feature/...`), replace with the Forgejo PR step above. -### Day 14 Rebaseline Plan (ACTIVE) +### Day 15 Rebaseline Plan (ACTIVE) This section replaces all unfinished work items with the Day 14 plan. Work is organized by milestone and is the authoritative checklist from this point forward. #### M1: Minimal Local Source-Code Workflow (Target: Day 7, recovery path) **Parallel Group M1: Tool-Aware Execute + Sandbox Apply** -- [X] **COMMIT (Owner: Jeff | Group: M1.actor-runtime | Branch: feature/m1-actor-runtime | Planned: Day 14 | Expected: Day 16) - Commit message: "feat(actor): add tool-calling runtime for execution actors"** +- [X] **COMMIT (Owner: Jeff | Group: M1.actor-runtime | Branch: feature/m1-actor-runtime | Planned: Day 14 | Done: Day 14, February 22, 2026 08:30:28 +0000) - Commit message: "feat(actor): add tool-calling runtime for execution actors"** - [X] Git [Jeff]: `git checkout master` - [X] Git [Jeff]: `git pull origin master` - [X] Git [Jeff]: `git checkout -b feature/m1-actor-runtime` @@ -2276,7 +2316,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [X] Git [Jeff]: `git push -u origin feature/m1-actor-runtime` - [X] Forgejo PR [Jeff]: Open PR from `feature/m1-actor-runtime` to `master` with a suitable and thorough description (PR #141) -- [X] **COMMIT (Owner: Jeff | Group: M1.plan-execute | Branch: feature/m1-plan-execute-runtime | Planned: Day 15 | Expected: Day 17) - Commit message: "feat(plan): wire execute phase to actor runtime and changeset capture"** +- [X] **COMMIT (Owner: Jeff | Group: M1.plan-execute | Branch: feature/m1-plan-execute-runtime | Planned: Day 15 | Done: Day 14, February 22, 2026 15:13:43 +0000) - Commit message: "feat(M1.2): PlanExecutionContext, RuntimeExecuteActor, and runtime mode"** - [X] Git [Jeff]: `git checkout master` - [X] Git [Jeff]: `git pull origin master` - [X] Git [Jeff]: `git checkout -b feature/m1-plan-execute-runtime` @@ -2295,36 +2335,38 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [X] Git [Jeff]: `git push -u origin feature/m1-plan-execute-runtime` - [X] Forgejo PR [Jeff]: Open PR from `feature/m1-plan-execute-runtime` to `master` with a suitable and thorough description (PR #149) -- [ ] **COMMIT (Owner: Hamza | Group: M1.resource-handlers | Branch: feature/m1-resource-handlers | Planned: Day 14 | Expected: Day 16) - Commit message: "feat(resource): add handler runtime for git-checkout and fs-directory"** - - [ ] Git [Hamza]: `git checkout master` - - [ ] Git [Hamza]: `git pull origin master` - - [ ] Git [Hamza]: `git checkout -b feature/m1-resource-handlers` - - [ ] Code [Hamza]: Define a resource handler protocol and implement `GitCheckoutHandler` + `FsDirectoryHandler` resolution of local paths. - - [ ] Code [Hamza]: Integrate sandbox manager to provide per-plan sandbox roots for git worktrees and copy-on-write paths. - - [ ] Code [Hamza]: Add resource binding helper to pass resource root into tool inputs consistently. - - [ ] Docs [Hamza]: Update `docs/reference/resource_handlers.md` with handler behavior and sandbox outputs. - - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - - [ ] Tests (Behave) [Hamza]: Add scenarios for handler resolution and sandbox root creation. - - [ ] Tests (Robot) [Hamza]: Add Robot test covering resource handler + sandbox integration via CLI. - - [ ] Tests (ASV) [Hamza]: Add `benchmarks/resource_handler_bench.py` for handler resolution overhead. - - [ ] Quality [Hamza]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. - - [ ] Quality [Hamza]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it. - - [ ] Git [Hamza]: Perform an appropriate `git add` command to add all the files that should be part of this commit to the git index - - [ ] Git [Hamza]: `git commit -m "feat(resource): add handler runtime for git-checkout and fs-directory"` - - [ ] Git [Hamza]: `git push -u origin feature/m1-resource-handlers` - - [ ] Forgejo PR [Hamza]: Open PR from `feature/m1-resource-handlers` to `master` with a suitable and thorough description +- [x] **COMMIT (Owner: Hamza | Group: M1.resource-handlers | Branch: feature/m1-resource-handlers | Planned: Day 14 | Expected: Day 16) - Commit message: "feat(resource): add handler runtime for git-checkout and fs-directory"** + - [x] Git [Hamza]: `git checkout master` + - [x] Git [Hamza]: `git pull origin master` + - [x] Git [Hamza]: `git checkout -b feature/m1-resource-handlers` + - [x] Code [Hamza]: Define a resource handler protocol and implement `GitCheckoutHandler` + `FsDirectoryHandler` resolution of local paths. + - [x] Code [Hamza]: Integrate sandbox manager to provide per-plan sandbox roots for git worktrees and copy-on-write paths. + - [x] Code [Hamza]: Add resource binding helper to pass resource root into tool inputs consistently. + - [x] Docs [Hamza]: Update `docs/reference/resource_handlers.md` with handler behavior and sandbox outputs. + - [x] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) + - [x] Tests (Behave) [Hamza]: Add scenarios for handler resolution and sandbox root creation. + - [x] Tests (Robot) [Hamza]: Add Robot test covering resource handler + sandbox integration via CLI. + - [x] Tests (ASV) [Hamza]: Add `benchmarks/resource_handler_bench.py` for handler resolution overhead. + - [x] Quality [Hamza]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. + - [x] Quality [Hamza]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it. + - [x] Git [Hamza]: Perform an appropriate `git add` command to add all the files that should be part of this commit to the git index + - [x] Git [Hamza]: `git commit -m "feat(resource): add handler runtime for git-checkout and fs-directory"` + - [x] Git [Hamza]: `git push -u origin feature/m1-resource-handlers` + - [x] Forgejo PR [Hamza]: Open PR from `feature/m1-resource-handlers` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Luis | Group: M1.apply-pipeline | Branch: feature/m1-apply-pipeline | Planned: Day 15 | Expected: Day 17) - Commit message: "feat(apply): merge sandbox changes into targets with conflict handling"** +- [ ] **COMMIT (Owner: Luis | Group: M1.apply-pipeline | Branch: feature/m1-apply-pipeline | Planned: Day 15 | Expected: Day 18) - Commit message: "feat(apply): merge sandbox changes into targets with conflict handling"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m1-apply-pipeline` - - [ ] Code [Luis]: Implement sandbox merge/apply pipeline for git worktree + copy-on-write strategies with conflict detection. - - [ ] Code [Luis]: Wire PlanApplyService to apply pipeline and record apply summaries (files changed, validations run). - - [ ] Code [Luis]: Add validation gating hook (skip if no validations attached; block on required failures). + - [ ] Code [Luis]: Implement sandbox merge/apply pipeline for git worktree + copy-on-write strategies with explicit conflict detection and rollback hooks. + - [ ] Code [Luis]: Wire `PlanApplyService` to the merge pipeline and persist apply summary fields (files changed, validations run, conflict details). + - [ ] Code [Luis]: Add validation gating hook: gather ValidationAttachments for the plan resources, run required validations, block apply on failures, record results. + - [ ] Code [Luis]: Add `--allow-empty` apply flag behavior aligned with `PlanApplyService.guard_empty_changeset`. + - [ ] Code [Luis]: Ensure apply transitions set terminal states (`applied`, `constrained`, `errored`, `cancelled`) with error details per spec. - [ ] Docs [Luis]: Update `docs/reference/plan_apply.md` with apply pipeline and conflict outcomes. - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - - [ ] Tests (Behave) [Luis]: Add scenarios for apply success, conflict failure, and validation-gated apply. - - [ ] Tests (Robot) [Luis]: Add Robot CLI test covering `plan diff` + `plan apply` with sandbox merge. + - [ ] Tests (Behave) [Luis]: Add scenarios for apply success, conflict failure, empty changeset guard, and validation-gated apply. + - [ ] Tests (Robot) [Luis]: Add Robot CLI test covering `plan diff` + `plan apply --yes` with sandbox merge and validation output. - [ ] Tests (ASV) [Luis]: Add `benchmarks/plan_apply_bench.py` for apply runtime baseline. - [ ] Quality [Luis]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. - [ ] Quality [Luis]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it. @@ -2333,15 +2375,16 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Luis]: `git push -u origin feature/m1-apply-pipeline` - [ ] Forgejo PR [Luis]: Open PR from `feature/m1-apply-pipeline` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Brent | Group: M1.e2e | Branch: feature/m1-e2e-sourcecode | Planned: Day 16 | Expected: Day 17) - Commit message: "test(e2e): add M1 source-code plan lifecycle suite"** +- [ ] **COMMIT (Owner: Brent | Group: M1.e2e | Branch: feature/m1-e2e-sourcecode | Planned: Day 16 | Expected: Day 18) - Commit message: "test(e2e): add M1 source-code plan lifecycle suite"** - [ ] Git [Brent]: `git checkout master` - [ ] Git [Brent]: `git pull origin master` - [ ] Git [Brent]: `git checkout -b feature/m1-e2e-sourcecode` - - [ ] Code [Brent]: Add E2E fixtures for a minimal git repo and action config used in the M1 flow. - - [ ] Docs [Brent]: Update `docs/development/testing.md` with M1 source-code smoke run instructions. + - [ ] Code [Brent]: Add E2E fixtures for a minimal git repo, a `git-checkout` resource config, and a minimal action YAML with strategy/execution actors. + - [ ] Code [Brent]: Add helper steps that create a temp project + resource link and capture plan IDs for subsequent CLI steps. + - [ ] Docs [Brent]: Update `docs/development/testing.md` with M1 source-code smoke run instructions and failure triage tips. - [ ] Git [Brent]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - - [ ] Tests (Behave) [Brent]: Add `features/m1_sourcecode_smoke.feature` covering action create, plan use, execute, diff, apply. - - [ ] Tests (Robot) [Brent]: Add Robot CLI smoke suite for M1 end-to-end flow. + - [ ] Tests (Behave) [Brent]: Add `features/m1_sourcecode_smoke.feature` covering action create, project/resource link, plan use, execute, diff, apply. + - [ ] Tests (Robot) [Brent]: Add Robot CLI smoke suite for M1 end-to-end flow using `--format plain` to stabilize assertions. - [ ] Tests (ASV) [Brent]: Add `benchmarks/m1_sourcecode_smoke_bench.py` for baseline runtime. - [ ] Quality [Brent]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. - [ ] Quality [Brent]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it. @@ -2355,16 +2398,18 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or **SEQUENTIAL ORDER**: M2.1 (hierarchical actor YAML) -> M2.2 (actor compiler/runtime) -- [ ] **COMMIT (Owner: Aditya | Group: M2.1.actor-yaml | Branch: feature/m2-actor-yaml | Planned: Day 15 | Expected: Day 16) - Commit message: "feat(actor): extend hierarchical actor YAML schema and loader"** +- [ ] **COMMIT (Owner: Aditya | Group: M2.1.actor-yaml | Branch: feature/m2-actor-yaml | Planned: Day 15 | Expected: Day 19) - Commit message: "feat(actor): extend hierarchical actor YAML schema and loader"** - [ ] Git [Aditya]: `git checkout master` - [ ] Git [Aditya]: `git pull origin master` - [ ] Git [Aditya]: `git checkout -b feature/m2-actor-yaml` - - [ ] Code [Aditya]: Extend actor YAML schema to support hierarchical graphs (nodes, edges, subgraphs, entry/exit), per-node LSP bindings, and tool-source references. - - [ ] Code [Aditya]: Update actor loader validation errors with precise field paths and resolution hints. + - [ ] Code [Aditya]: Extend actor YAML schema to support hierarchical graphs (nodes, edges, subgraphs, entry/exit) with explicit node types (`llm`, `tool`, `router`). + - [ ] Code [Aditya]: Add per-node LSP bindings (`lsp_servers`, `languages`, `auto_detect`) and tool-source references (`skills`, `mcp_servers`, `agent_skills`). + - [ ] Code [Aditya]: Add schema validation for namespaced actor references, duplicate node IDs, and edge target existence. + - [ ] Code [Aditya]: Update actor loader validation errors with precise field paths, remediation hints, and line/column reporting. - [ ] Docs [Aditya]: Update `docs/reference/actor_config.md` with hierarchical examples and error cases. - [ ] Docs [Aditya]: Add/refresh `examples/actors/` hierarchical YAML samples. - [ ] Git [Aditya]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - - [ ] Tests (Behave) [Aditya]: Add scenarios for graph validation (missing nodes, cycles, invalid edge refs, missing LSP bindings). + - [ ] Tests (Behave) [Aditya]: Add scenarios for graph validation (missing nodes, cycles, invalid edge refs, invalid LSP binding refs, duplicate node IDs). - [ ] Tests (Robot) [Aditya]: Add Robot smoke test loading hierarchical actor YAML via CLI. - [ ] Tests (ASV) [Aditya]: Add `benchmarks/actor_yaml_bench.py` for schema load/validation overhead. - [ ] Quality [Aditya]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. @@ -2374,17 +2419,18 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Aditya]: `git push -u origin feature/m2-actor-yaml` - [ ] Forgejo PR [Aditya]: Open PR from `feature/m2-actor-yaml` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Jeff | Group: M2.2.actor-compiler | Branch: feature/m3-actor-compiler | Planned: Day 16 | Expected: Day 17) - Commit message: "feat(actor): compile hierarchical actor configs to LangGraph"** +- [ ] **COMMIT (Owner: Jeff | Group: M2.2.actor-compiler | Branch: feature/m3-actor-compiler | Planned: Day 16 | Expected: Day 20) - Commit message: "feat(actor): compile hierarchical actor configs to LangGraph"** - [ ] Git [Jeff]: `git checkout master` - [ ] Git [Jeff]: `git pull origin master` - [ ] Git [Jeff]: `git checkout -b feature/m3-actor-compiler` - - [ ] Code [Jeff]: Implement actor compiler to translate hierarchical YAML graphs into LangGraph StateGraph nodes/edges with LSP bindings. - - [ ] Code [Jeff]: Add actor reference/subgraph resolution with explicit validation errors for missing nodes or cycles. - - [ ] Code [Jeff]: Add runtime wiring so PlanExecutor can execute compiled actors using the tool runtime and the action-configured roles. + - [ ] Code [Jeff]: Implement actor compiler to translate hierarchical YAML graphs into LangGraph StateGraph nodes/edges with LSP bindings applied per node. + - [ ] Code [Jeff]: Add subgraph resolution with cycle detection and clear errors for missing nodes or invalid entry/exit mapping. + - [ ] Code [Jeff]: Add runtime wiring so `PlanExecutor` can execute compiled actors using the tool runtime and action-configured roles. + - [ ] Code [Jeff]: Add compiler output metadata (node IDs, tool nodes, LSP bindings) for diagnostics and CLI inspection. - [ ] Docs [Jeff]: Update `docs/reference/actor_compiler.md` with compilation pipeline, node binding, and error modes. - [ ] Git [Jeff]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - - [ ] Tests (Behave) [Jeff]: Add scenarios for compile success/failure and node wiring validation. - - [ ] Tests (Robot) [Jeff]: Add Robot smoke test compiling and running a multi-node actor graph. + - [ ] Tests (Behave) [Jeff]: Add scenarios for compile success/failure, subgraph resolution, and node wiring validation. + - [ ] Tests (Robot) [Jeff]: Add Robot smoke test compiling and running a multi-node actor graph with LSP bindings stubbed. - [ ] Tests (ASV) [Jeff]: Add `benchmarks/actor_compiler_bench.py` for compile overhead. - [ ] Quality [Jeff]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. - [ ] Quality [Jeff]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it. @@ -2393,15 +2439,17 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Jeff]: `git push -u origin feature/m3-actor-compiler` - [ ] Forgejo PR [Jeff]: Open PR from `feature/m3-actor-compiler` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Aditya | Group: M2.3.mcp-runtime | Branch: feature/m3-mcp-adapter | Planned: Day 16 | Expected: Day 18) - Commit message: "feat(skill): add MCP adapter for external tools"** +- [ ] **COMMIT (Owner: Aditya | Group: M2.3.mcp-runtime | Branch: feature/m3-mcp-adapter | Planned: Day 16 | Expected: Day 20) - Commit message: "feat(skill): add MCP adapter for external tools"** - [ ] Git [Aditya]: `git checkout master` - [ ] Git [Aditya]: `git pull origin master` - [ ] Git [Aditya]: `git checkout -b feature/m3-mcp-adapter` - - [ ] Code [Aditya]: Implement MCP adapter runtime to connect to servers, enumerate tools, and register them in ToolRegistry. + - [ ] Code [Aditya]: Implement MCP adapter runtime to connect to servers, enumerate tools, and register them in ToolRegistry with `source=mcp`. + - [ ] Code [Aditya]: Map MCP tool schemas to internal ToolSpec (inputs/outputs, read_only/writes/checkpointable flags). - [ ] Code [Aditya]: Add invocation path that validates tool inputs/outputs against MCP schema and surfaces errors consistently. + - [ ] Code [Aditya]: Add connection lifecycle management (connect, reconnect, teardown) with safe timeouts. - [ ] Docs [Aditya]: Update `docs/reference/mcp_adapter.md` with runtime configuration, lifecycle, and error handling. - [ ] Git [Aditya]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - - [ ] Tests (Behave) [Aditya]: Add scenarios for MCP tool discovery, invocation, and error mapping. + - [ ] Tests (Behave) [Aditya]: Add scenarios for MCP tool discovery, invocation, error mapping, and reconnect behavior. - [ ] Tests (Robot) [Aditya]: Add Robot integration test using a local MCP test server stub. - [ ] Tests (ASV) [Aditya]: Add `benchmarks/mcp_runtime_bench.py` for adapter overhead. - [ ] Quality [Aditya]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. @@ -2411,16 +2459,17 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Aditya]: `git push -u origin feature/m3-mcp-adapter` - [ ] Forgejo PR [Aditya]: Open PR from `feature/m3-mcp-adapter` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Aditya | Group: M2.4a.agent-skills-loader | Branch: feature/m3-agent-skills-loader | Planned: Day 16 | Expected: Day 18) - Commit message: "feat(skill): add agent skills loader"** +- [ ] **COMMIT (Owner: Aditya | Group: M2.4a.agent-skills-loader | Branch: feature/m3-agent-skills-loader | Planned: Day 16 | Expected: Day 20) - Commit message: "feat(skill): add agent skills loader"** - [ ] Git [Aditya]: `git checkout master` - [ ] Git [Aditya]: `git pull origin master` - [ ] Git [Aditya]: `git checkout -b feature/m3-agent-skills-loader` - [ ] Code [Aditya]: Add `AgentSkillSpec` loader that parses `SKILL.md` frontmatter + progressive disclosure sections into structured steps with stable ordering. - [ ] Code [Aditya]: Map Agent Skills to Tool/Validation definitions with namespaced naming, resource binding slots, and read-only defaults. - [ ] Code [Aditya]: Support `scripts/`, `references/`, and `assets/` folders with path normalization and safe read-only access rules. + - [ ] Code [Aditya]: Add explicit validation for missing frontmatter fields (name, description, steps) with actionable errors. - [ ] Docs [Aditya]: Add `docs/reference/agent_skills.md` describing folder layout, parsing rules, and tool mapping. - [ ] Git [Aditya]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - - [ ] Tests (Behave) [Aditya]: Add scenarios for valid/invalid SKILL.md parsing, namespaced naming, and step ordering. + - [ ] Tests (Behave) [Aditya]: Add scenarios for valid/invalid SKILL.md parsing, namespaced naming, step ordering, and missing frontmatter errors. - [ ] Tests (Robot) [Aditya]: Add Robot test that loads a sample Agent Skills folder and verifies tool metadata. - [ ] Tests (ASV) [Aditya]: Add `benchmarks/agent_skills_loader_bench.py` for parsing throughput. - [ ] Quality [Aditya]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. @@ -2430,13 +2479,14 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Aditya]: `git push -u origin feature/m3-agent-skills-loader` - [ ] Forgejo PR [Aditya]: Open PR from `feature/m3-agent-skills-loader` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Jeff | Group: M2.4b.agent-skills-registry | Branch: feature/m3-agent-skills-registry | Planned: Day 17 | Expected: Day 18) - Commit message: "feat(skill): integrate agent skills discovery"** +- [ ] **COMMIT (Owner: Jeff | Group: M2.4b.agent-skills-registry | Branch: feature/m3-agent-skills-registry | Planned: Day 17 | Expected: Day 21) - Commit message: "feat(skill): integrate agent skills discovery"** - [ ] Git [Jeff]: `git checkout master` - [ ] Git [Jeff]: `git pull origin master` - [ ] Git [Jeff]: `git checkout -b feature/m3-agent-skills-registry` - [ ] Code [Jeff]: Add config key `skills.agent_skills_paths` with default discovery path and allow multiple folders. - [ ] Code [Jeff]: Register Agent Skills tools in ToolRegistry with `source=agent_skills` and include source metadata in `agents skill tools` output. - [ ] Code [Jeff]: Add discovery refresh hook in SkillRegistryService and CLI `agents skill tools` to optionally re-scan Agent Skills paths. + - [ ] Code [Jeff]: Add explicit conflict handling when Agent Skills names collide with existing ToolRegistry entries. - [ ] Docs [Jeff]: Update `docs/reference/skill_registry.md` and CLI reference with Agent Skills discovery behavior and output fields. - [ ] Git [Jeff]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Jeff]: Add scenarios for discovery, refresh, and source metadata rendering. @@ -2449,34 +2499,35 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Jeff]: `git push -u origin feature/m3-agent-skills-registry` - [ ] Forgejo PR [Jeff]: Open PR from `feature/m3-agent-skills-registry` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Hamza | Group: M2.5.resource-registry | Branch: feature/m2-resource-registry | Planned: Day 16 | Expected: Day 18) - Commit message: "feat(resource): add resource registry and DAG metadata"** - - [ ] Git [Hamza]: `git checkout master` - - [ ] Git [Hamza]: `git pull origin master` - - [ ] Git [Hamza]: `git checkout -b feature/m2-resource-registry` - - [ ] Code [Hamza]: Implement ResourceType and Resource registries with DAG parent/child constraints and discovery metadata. - - [ ] Code [Hamza]: Add local-mode discovery stubs and validation hooks for resource graph integrity. - - [ ] Docs [Hamza]: Update `docs/reference/resources.md` with registry fields and discovery behavior. - - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - - [ ] Tests (Behave) [Hamza]: Add scenarios for registry validation, DAG constraints, and discovery defaults. - - [ ] Tests (Robot) [Hamza]: Add Robot test verifying resource registry CLI output. - - [ ] Tests (ASV) [Hamza]: Add `benchmarks/resource_registry_bench.py` for registry lookup overhead. - - [ ] Quality [Hamza]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. - - [ ] Quality [Hamza]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it. - - [ ] Git [Hamza]: Perform an appropriate `git add` command to add all the files that should be part of this commit to the git index - - [ ] Git [Hamza]: `git commit -m "feat(resource): add resource registry and DAG metadata"` - - [ ] Git [Hamza]: `git push -u origin feature/m2-resource-registry` - - [ ] Forgejo PR [Hamza]: Open PR from `feature/m2-resource-registry` to `master` with a suitable and thorough description +- [x] **COMMIT (Owner: Hamza | Group: M2.5.resource-registry | Branch: feature/m2-resource-registry | Planned: Day 16 | Expected: Day 18) - Commit message: "feat(resource): add resource registry and DAG metadata"** + - [x] Git [Hamza]: `git checkout master` + - [x] Git [Hamza]: `git pull origin master` + - [x] Git [Hamza]: `git checkout -b feature/m2-resource-registry` + - [x] Code [Hamza]: Implement ResourceType and Resource registries with DAG parent/child constraints and discovery metadata. + - [x] Code [Hamza]: Add local-mode discovery stubs and validation hooks for resource graph integrity. + - [x] Docs [Hamza]: Update `docs/reference/resources.md` with registry fields and discovery behavior. + - [x] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) + - [x] Tests (Behave) [Hamza]: Add scenarios for registry validation, DAG constraints, and discovery defaults. + - [x] Tests (Robot) [Hamza]: Add Robot test verifying resource registry CLI output. + - [x] Tests (ASV) [Hamza]: Add `benchmarks/resource_registry_bench.py` for registry lookup overhead. + - [x] Quality [Hamza]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. + - [x] Quality [Hamza]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it. + - [x] Git [Hamza]: Perform an appropriate `git add` command to add all the files that should be part of this commit to the git index + - [x] Git [Hamza]: `git commit -m "feat(resource): add resource registry and DAG metadata"` + - [x] Git [Hamza]: `git push -u origin feature/m2-resource-registry` + - [x] Forgejo PR [Hamza]: Open PR from `feature/m2-resource-registry` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Luis | Group: M2.6.changeset-persistence | Branch: feature/m2-changeset-persistence | Planned: Day 16 | Expected: Day 18) - Commit message: "feat(changeset): persist changesets and diff artifacts"** +- [ ] **COMMIT (Owner: Luis | Group: M2.6.changeset-persistence | Branch: feature/m2-changeset-persistence | Planned: Day 16 | Expected: Day 21) - Commit message: "feat(changeset): persist changesets and diff artifacts"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m2-changeset-persistence` - [ ] Code [Luis]: Implement ChangeSet persistence with diff artifacts and tool invocation metadata. - [ ] Code [Luis]: Wire ChangeSet retrieval into `plan diff` and `plan status` outputs. + - [ ] Code [Luis]: Add ChangeSet cleanup on plan cancel and apply failure paths. - [ ] Docs [Luis]: Update `docs/reference/changeset.md` with persistence fields and diff output examples. - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - - [ ] Tests (Behave) [Luis]: Add scenarios for ChangeSet persistence, diff rendering, and missing artifact handling. - - [ ] Tests (Robot) [Luis]: Add Robot tests for ChangeSet retrieval via CLI. + - [ ] Tests (Behave) [Luis]: Add scenarios for ChangeSet persistence, diff rendering, missing artifact handling, and cleanup on cancel. + - [ ] Tests (Robot) [Luis]: Add Robot tests for ChangeSet retrieval via CLI `plan artifacts` output. - [ ] Tests (ASV) [Luis]: Add `benchmarks/changeset_persistence_bench.py` for persistence overhead. - [ ] Quality [Luis]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. - [ ] Quality [Luis]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it. @@ -2600,12 +2651,13 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Aditya]: `git push -u origin feature/m4-skill-registry-refresh` - [ ] Forgejo PR [Aditya]: Open PR from `feature/m4-skill-registry-refresh` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Brent | Group: M2.tests | Branch: feature/m2-actor-tool-smoke | Planned: Day 17 | Expected: Day 18) - Commit message: "test(e2e): add M2 actor + tool source smoke suite"** +- [ ] **COMMIT (Owner: Brent | Group: M2.tests | Branch: feature/m2-actor-tool-smoke | Planned: Day 17 | Expected: Day 22) - Commit message: "test(e2e): add M2 actor + tool source smoke suite"** - [ ] Git [Brent]: `git checkout master` - [ ] Git [Brent]: `git pull origin master` - [ ] Git [Brent]: `git checkout -b feature/m2-actor-tool-smoke` - [ ] Code [Brent]: Add fixtures for hierarchical actor YAML, MCP stub server, and skill packs. - - [ ] Docs [Brent]: Update `docs/development/testing.md` with M2 smoke suite usage. + - [ ] Code [Brent]: Add helper steps for starting/stopping the MCP stub server and cleaning temp directories. + - [ ] Docs [Brent]: Update `docs/development/testing.md` with M2 smoke suite usage and MCP stub bootstrap steps. - [ ] Git [Brent]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Brent]: Add `features/m2_actor_tool_smoke.feature` covering actor compile, MCP tool invocation, and skill registry. - [ ] Tests (Robot) [Brent]: Add Robot CLI smoke suite for actor compile + plan execute. @@ -2622,24 +2674,24 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or **SEQUENTIAL ORDER**: M3.1 -> M3.2 -> M3.3 (decision persistence before CLI explain/tree) -- [ ] **COMMIT (Owner: Hamza | Group: M3.1.decision-domain | Branch: feature/m4-decision-domain | Planned: Day 19 | Expected: Day 20) - Commit message: "feat(decision): add decision domain and context snapshots"** - - [ ] Git [Hamza]: `git checkout master` - - [ ] Git [Hamza]: `git pull origin master` - - [ ] Git [Hamza]: `git checkout -b feature/m4-decision-domain` - - [ ] Code [Hamza]: Implement decision domain model and context snapshot structures per spec. - - [ ] Docs [Hamza]: Update `docs/reference/decision_model.md` with decision fields and lifecycle. - - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - - [ ] Tests (Behave) [Hamza]: Add scenarios for decision model validation and serialization. - - [ ] Tests (Robot) [Hamza]: Add Robot test for decision model persistence contract. - - [ ] Tests (ASV) [Hamza]: Add `benchmarks/decision_model_bench.py` for model overhead. - - [ ] Quality [Hamza]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. - - [ ] Quality [Hamza]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it. - - [ ] Git [Hamza]: Perform an appropriate `git add` command to add all the files that should be part of this commit to the git index - - [ ] Git [Hamza]: `git commit -m "feat(decision): add decision domain and context snapshots"` - - [ ] Git [Hamza]: `git push -u origin feature/m4-decision-domain` - - [ ] Forgejo PR [Hamza]: Open PR from `feature/m4-decision-domain` to `master` with a suitable and thorough description +- [x] **COMMIT (Owner: Hamza | Group: M3.1.decision-domain | Branch: feature/m4-decision-domain | Planned: Day 19 | Expected: Day 20) - Commit message: "feat(decision): add decision domain and context snapshots"** + - [x] Git [Hamza]: `git checkout master` + - [x] Git [Hamza]: `git pull origin master` + - [x] Git [Hamza]: `git checkout -b feature/m4-decision-domain` + - [x] Code [Hamza]: Implement decision domain model and context snapshot structures per spec. + - [x] Docs [Hamza]: Update `docs/reference/decision_model.md` with decision fields and lifecycle. + - [x] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) + - [x] Tests (Behave) [Hamza]: Add scenarios for decision model validation and serialization. + - [x] Tests (Robot) [Hamza]: Add Robot test for decision model persistence contract. + - [x] Tests (ASV) [Hamza]: Add `benchmarks/decision_model_bench.py` for model overhead. + - [x] Quality [Hamza]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. + - [x] Quality [Hamza]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it. + - [x] Git [Hamza]: Perform an appropriate `git add` command to add all the files that should be part of this commit to the git index + - [x] Git [Hamza]: `git commit -m "feat(decision): add decision domain and context snapshots"` + - [x] Git [Hamza]: `git push -u origin feature/m4-decision-domain` + - [x] Forgejo PR [Hamza]: Open PR from `feature/m4-decision-domain` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Hamza | Group: M3.2.decision-persistence | Branch: feature/m4-decision-persistence | Planned: Day 20 | Expected: Day 21) - Commit message: "feat(decision): add decision persistence"** +- [ ] **COMMIT (Owner: Hamza | Group: M3.2.decision-persistence | Branch: feature/m4-decision-persistence | Planned: Day 20 | Expected: Day 28) - Commit message: "feat(decision): add decision persistence"** - [ ] Git [Hamza]: `git checkout master` - [ ] Git [Hamza]: `git pull origin master` - [ ] Git [Hamza]: `git checkout -b feature/m4-decision-persistence` @@ -2658,7 +2710,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Hamza]: `git push -u origin feature/m4-decision-persistence` - [ ] Forgejo PR [Hamza]: Open PR from `feature/m4-decision-persistence` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Hamza | Group: M3.2b.decision-service | Branch: feature/m4-decision-service | Planned: Day 22 | Expected: Day 26) - Commit message: "feat(service): add decision recording and snapshot store"** +- [ ] **COMMIT (Owner: Hamza | Group: M3.2b.decision-service | Branch: feature/m4-decision-service | Planned: Day 22 | Expected: Day 29) - Commit message: "feat(service): add decision recording and snapshot store"** - [ ] Git [Hamza]: `git checkout master` - [ ] Git [Hamza]: `git pull origin master` - [ ] Git [Hamza]: `git checkout -b feature/m4-decision-service` @@ -2669,13 +2721,13 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Tests (Robot) [Hamza]: Add `robot/decision_recording.robot` integration smoke tests. - [ ] Tests (ASV) [Hamza]: Add `benchmarks/decision_recording_bench.py` for record throughput. - [ ] Quality [Hamza]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. - - [ ] Quality [Hamza]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes. + - [ ] Quality [Hamza]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it. - [ ] Git [Hamza]: Perform an appropriate `git add` command to add all the files that should be part of this commit to the git index - [ ] Git [Hamza]: `git commit -m "feat(service): add decision recording and snapshot store"` - [ ] Git [Hamza]: `git push -u origin feature/m4-decision-service` - [ ] Forgejo PR [Hamza]: Open PR from `feature/m4-decision-service` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Luis | Group: M3.2c.decision-di | Branch: feature/m4-decision-di | Planned: Day 26 | Expected: Day 26) - Commit message: "feat(di): wire decision services"** +- [ ] **COMMIT (Owner: Luis | Group: M3.2c.decision-di | Branch: feature/m4-decision-di | Planned: Day 26 | Expected: Day 30) - Commit message: "feat(di): wire decision services"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m4-decision-di` @@ -2689,11 +2741,11 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Quality [Luis]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. - [ ] Quality [Luis]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it. - [ ] Git [Luis]: Perform an appropriate `git add` command to add all the files that should be part of this commit to the git index - - [ ] Git [Luis]: `git commit -m "feat(di): wire decision services"`. + - [ ] Git [Luis]: `git commit -m "feat(di): wire decision services"` - [ ] Git [Luis]: `git push -u origin feature/m4-decision-di` - [ ] Forgejo PR [Luis]: Open PR from `feature/m4-decision-di` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Hamza | Group: M3.3.plan-explain | Branch: feature/m4-decision-cli | Planned: Day 20 | Expected: Day 21) - Commit message: "feat(cli): add plan explain and decision tree outputs"** +- [ ] **COMMIT (Owner: Hamza | Group: M3.3.plan-explain | Branch: feature/m4-decision-cli | Planned: Day 20 | Expected: Day 29) - Commit message: "feat(cli): add plan explain and decision tree outputs"** - [ ] Git [Hamza]: `git checkout master` - [ ] Git [Hamza]: `git pull origin master` - [ ] Git [Hamza]: `git checkout -b feature/m4-decision-cli` @@ -2710,7 +2762,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Hamza]: `git push -u origin feature/m4-decision-cli` - [ ] Forgejo PR [Hamza]: Open PR from `feature/m4-decision-cli` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Luis | Group: M3.4a.validation-pipeline | Branch: feature/m3-validation-pipeline | Planned: Day 21 | Expected: Day 22) - Commit message: "feat(validation): add validation pipeline and results model"** +- [ ] **COMMIT (Owner: Luis | Group: M3.4a.validation-pipeline | Branch: feature/m3-validation-pipeline | Planned: Day 21 | Expected: Day 28) - Commit message: "feat(validation): add validation pipeline and results model"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m3-validation-pipeline` @@ -2737,7 +2789,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Luis]: `git push -u origin feature/m3-validation-pipeline` - [ ] Forgejo PR [Luis]: Open PR from `feature/m3-validation-pipeline` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Luis | Group: M3.4.validation-runner | Branch: feature/m3-validation-apply | Planned: Day 20 | Expected: Day 22) - Commit message: "feat(validation): add validation runner and apply gating"** +- [ ] **COMMIT (Owner: Luis | Group: M3.4.validation-runner | Branch: feature/m3-validation-apply | Planned: Day 20 | Expected: Day 29) - Commit message: "feat(validation): add validation runner and apply gating"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m3-validation-apply` @@ -2776,7 +2828,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [X] Git [Jeff]: `git push -u origin feature/m3-invariants` - [X] Forgejo PR [Jeff]: Open PR from `feature/m3-invariants` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Luis | Group: M3.6.definition-of-done | Branch: feature/m4-definition-of-done | Planned: Day 26 | Expected: Day 26) - Commit message: "feat(dod): enforce definition-of-done gating"** +- [ ] **COMMIT (Owner: Luis | Group: M3.6.definition-of-done | Branch: feature/m4-definition-of-done | Planned: Day 26 | Expected: Day 30) - Commit message: "feat(dod): enforce definition-of-done gating"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m4-definition-of-done` @@ -2794,11 +2846,11 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Quality [Luis]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. - [ ] Quality [Luis]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it. - [ ] Git [Luis]: Perform an appropriate `git add` command to add all the files that should be part of this commit to the git index - - [ ] Git [Luis]: `git commit -m "feat(dod): enforce definition-of-done gating"`. + - [ ] Git [Luis]: `git commit -m "feat(dod): enforce definition-of-done gating"` - [ ] Git [Luis]: `git push -u origin feature/m4-definition-of-done` - [ ] Forgejo PR [Luis]: Open PR from `feature/m4-definition-of-done` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Brent | Group: M3.tests | Branch: feature/m3-decision-validation-smoke | Planned: Day 21 | Expected: Day 22) - Commit message: "test(e2e): add M3 decision + validation suites"** +- [ ] **COMMIT (Owner: Brent | Group: M3.tests | Branch: feature/m3-decision-validation-smoke | Planned: Day 21 | Expected: Day 30) - Commit message: "test(e2e): add M3 decision + validation suites"** - [ ] Git [Brent]: `git checkout master` - [ ] Git [Brent]: `git pull origin master` - [ ] Git [Brent]: `git checkout -b feature/m3-decision-validation-smoke` @@ -2815,21 +2867,21 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Brent]: `git push -u origin feature/m3-decision-validation-smoke` - [ ] Forgejo PR [Brent]: Open PR from `feature/m3-decision-validation-smoke` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Brent | Group: M3.7.decision-tests | Branch: feature/m4-decision-tests | Planned: Day 27 | Expected: Day 27) - Commit message: "test(persistence): add decision persistence suites"** +- [ ] **COMMIT (Owner: Brent | Group: M3.7.decision-tests | Branch: feature/m4-decision-tests | Planned: Day 27 | Expected: Day 31) - Commit message: "test(persistence): add decision persistence suites"** - [ ] Git [Brent]: `git checkout master` - [ ] Git [Brent]: `git pull origin master` - [ ] Git [Brent]: `git checkout -b feature/m4-decision-tests` + - [ ] Git [Brent]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Brent]: Add `features/decision_persistence.feature` scenarios. - [ ] Tests (Behave) [Brent]: Add scenarios for superseded decisions, correction attempts, and tree depth filtering. - [ ] Tests (Behave) [Brent]: Add output snapshots for `plan explain --format json` and `plan tree --format yaml`. - [ ] Tests (Robot) [Brent]: Add `robot/decision_persistence.robot` E2E coverage. - [ ] Docs [Brent]: Update `docs/development/testing.md` with decision suites. - - [ ] Git [Brent]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (ASV) [Brent]: Add `benchmarks/decision_persistence_bench.py` for DB persistence throughput. - [ ] Quality [Brent]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. - [ ] Quality [Brent]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it. - [ ] Git [Brent]: Perform an appropriate `git add` command to add all the files that should be part of this commit to the git index - - [ ] Git [Brent]: `git commit -m "test(persistence): add decision persistence suites"`. + - [ ] Git [Brent]: `git commit -m "test(persistence): add decision persistence suites"` - [ ] Git [Brent]: `git push -u origin feature/m4-decision-tests` - [ ] Forgejo PR [Brent]: Open PR from `feature/m4-decision-tests` to `master` with a suitable and thorough description @@ -2873,7 +2925,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [X] Git [Jeff]: `git push -u origin feature/m4-correction-flows` - [X] Forgejo PR [Jeff]: Open PR from `feature/m4-correction-flows` to `master` with a suitable and thorough description (PR #147) -- [ ] **COMMIT (Owner: Hamza | Group: M4.3.checkpoints | Branch: feature/m4-checkpoints | Planned: Day 23 | Expected: Day 24) - Commit message: "feat(sandbox): add checkpoint and rollback hooks"** +- [ ] **COMMIT (Owner: Hamza | Group: M4.3.checkpoints | Branch: feature/m4-checkpoints | Planned: Day 23 | Expected: Day 31) - Commit message: "feat(sandbox): add checkpoint and rollback hooks"** - [ ] Git [Hamza]: `git checkout master` - [ ] Git [Hamza]: `git pull origin master` - [ ] Git [Hamza]: `git checkout -b feature/m4-checkpoints` @@ -2890,7 +2942,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Hamza]: `git push -u origin feature/m4-checkpoints` - [ ] Forgejo PR [Hamza]: Open PR from `feature/m4-checkpoints` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Luis | Group: M4.4.subplans | Branch: feature/m4-subplan-execution | Planned: Day 24 | Expected: Day 25) - Commit message: "feat(subplan): execute and merge subplans"** +- [ ] **COMMIT (Owner: Luis | Group: M4.4.subplans | Branch: feature/m4-subplan-execution | Planned: Day 24 | Expected: Day 32) - Commit message: "feat(subplan): execute and merge subplans"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m4-subplan-execution` @@ -2930,7 +2982,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [X] Git [Jeff]: `git push -u origin feature/m4-phase-reversion` - [X] Forgejo PR [Jeff]: Open PR from `feature/m4-phase-reversion` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Luis | Group: M4.6.error-recovery | Branch: feature/m4-error-recovery | Planned: Day 22 | Expected: Day 26) - Commit message: "feat(plan): add error recovery patterns and CLI hints"** +- [ ] **COMMIT (Owner: Luis | Group: M4.6.error-recovery | Branch: feature/m4-error-recovery | Planned: Day 22 | Expected: Day 33) - Commit message: "feat(plan): add error recovery patterns and CLI hints"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m4-error-recovery` @@ -2951,7 +3003,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Luis]: `git push -u origin feature/m4-error-recovery` - [ ] Forgejo PR [Luis]: Open PR from `feature/m4-error-recovery` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Brent | Group: M4.tests | Branch: feature/m4-correction-subplan-smoke | Planned: Day 24 | Expected: Day 25) - Commit message: "test(e2e): add M4 correction + subplan suites"** +- [ ] **COMMIT (Owner: Brent | Group: M4.tests | Branch: feature/m4-correction-subplan-smoke | Planned: Day 24 | Expected: Day 33) - Commit message: "test(e2e): add M4 correction + subplan suites"** - [ ] Git [Brent]: `git checkout master` - [ ] Git [Brent]: `git pull origin master` - [ ] Git [Brent]: `git checkout -b feature/m4-correction-subplan-smoke` @@ -2971,7 +3023,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or #### M5: ACMS v1 + Context Scaling (Day 26) **Parallel Group M5: ACMS Pipeline + Context Indexing + Automation Profiles** -- [ ] **COMMIT (Owner: Hamza | Group: M5.1.acms-context | Branch: feature/m5-acms-context | Planned: Day 26 | Expected: Day 28) - Commit message: "feat(acms): add ACMS v1 context pipeline"** +- [ ] **COMMIT (Owner: Hamza | Group: M5.1.acms-context | Branch: feature/m5-acms-context | Planned: Day 26 | Expected: Day 33) - Commit message: "feat(acms): add ACMS v1 context pipeline"** - [ ] Git [Hamza]: `git checkout master` - [ ] Git [Hamza]: `git pull origin master` - [ ] Git [Hamza]: `git checkout -b feature/m5-acms-context` @@ -2990,11 +3042,10 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or **Parallel Group M5b: ACMS Component Buildout (UKO + CRP + Strategy + Fusion + Scoped Views + Skeleton)** -- [ ] **COMMIT (Owner: Hamza | Group: ACMS1.uko | Branch: feature/m6-acms-uko-schema | Planned: Day 26 | Expected: Day 31) - Commit message: "feat(uko): add UKO ontology scaffolding"** +- [ ] **COMMIT (Owner: Hamza | Group: ACMS1.uko | Branch: feature/m6-acms-uko-schema | Planned: Day 26 | Expected: Day 34) - Commit message: "feat(uko): add UKO ontology scaffolding"** - [ ] Git [Hamza]: `git checkout master` - [ ] Git [Hamza]: `git pull origin master` - [ ] Git [Hamza]: `git checkout -b feature/m6-acms-uko-schema` - - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Hamza]: Add UKO Layer 0-3 ontology skeleton (RDF/TTL) under `docs/ontology/uko.ttl` with version headers. - [ ] Code [Hamza]: Define base URI, version IRI, and prefix conventions for layers and language-specific nodes. - [ ] Code [Hamza]: Add minimal Layer 0 nodes (Resource, Artifact, CodeArtifact, Document) to allow end-to-end parsing. @@ -3004,6 +3055,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Code [Hamza]: Add unit helper to resolve layer inheritance (Layer 3 -> 2 -> 1 -> 0). - [ ] Docs [Hamza]: Add `docs/reference/uko.md` describing layers, URI format, and extension points. - [ ] Docs [Hamza]: Add a minimal example ontology snippet and parsing walkthrough. + - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Hamza]: Add `features/uko_ontology.feature` for URI parsing and inheritance checks. - [ ] Tests (Robot) [Hamza]: Add `robot/uko_ontology.robot` smoke tests for ontology load. - [ ] Tests (ASV) [Hamza]: Add `benchmarks/uko_load_bench.py` for load throughput. @@ -3014,11 +3066,10 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Hamza]: `git push -u origin feature/m6-acms-uko-schema` - [ ] Forgejo PR [Hamza]: Open PR from `feature/m6-acms-uko-schema` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Jeff | Group: ACMS2.crp | Branch: feature/m6-acms-crp-models | Planned: Day 27 | Expected: Day 31) - Commit message: "feat(acms): add context request protocol models"** +- [ ] **COMMIT (Owner: Jeff | Group: ACMS2.crp | Branch: feature/m6-acms-crp-models | Planned: Day 27 | Expected: Day 34) - Commit message: "feat(acms): add context request protocol models"** - [ ] Git [Jeff]: `git checkout master` - [ ] Git [Jeff]: `git pull origin master` - [ ] Git [Jeff]: `git checkout -b feature/m6-acms-crp-models` - - [ ] Git [Jeff]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Jeff]: Add `ContextRequest`, `ContextFragment`, `DetailLevel`, and `ContextBudget` models with validation. - [ ] Code [Jeff]: Add built-in `builtin/context` skill with tools `request_context`, `query_history`, and `get_context_budget` (stubbed to ACMS). - [ ] Code [Jeff]: Add `ContextRequest` fields for `focus`, `breadth`, `depth`, `strategy`, `temporal_scope`, and `skeleton_ratio` with defaulting rules. @@ -3027,6 +3078,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Code [Jeff]: Add `ContextRequest` validation for invalid depth names and unknown strategies. - [ ] Docs [Jeff]: Add `docs/reference/crp.md` with request fields, detail levels, and examples. - [ ] Docs [Jeff]: Document `DetailLevelMap` resolution rules and defaults. + - [ ] Git [Jeff]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Jeff]: Add `features/crp_models.feature` for validation and serialization ordering. - [ ] Tests (Robot) [Jeff]: Add `robot/crp_models.robot` smoke tests. - [ ] Tests (ASV) [Jeff]: Add `benchmarks/crp_model_bench.py` for validation throughput. @@ -3037,11 +3089,10 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Jeff]: `git push -u origin feature/m6-acms-crp-models` - [ ] Forgejo PR [Jeff]: Open PR from `feature/m6-acms-crp-models` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Hamza | Group: ACMS3.strategy | Branch: feature/m6-acms-strategy-registry | Planned: Day 27 | Expected: Day 31) - Commit message: "feat(acms): add context strategy registry"** +- [ ] **COMMIT (Owner: Hamza | Group: ACMS3.strategy | Branch: feature/m6-acms-strategy-registry | Planned: Day 27 | Expected: Day 34) - Commit message: "feat(acms): add context strategy registry"** - [ ] Git [Hamza]: `git checkout master` - [ ] Git [Hamza]: `git pull origin master` - [ ] Git [Hamza]: `git checkout -b feature/m6-acms-strategy-registry` - - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Hamza]: Define `ContextStrategy` interface and StrategyRegistry with plugin discovery. - [ ] Code [Hamza]: Add stub strategies (keyword, semantic, graph, temporal) with feature flags and no-op defaults. - [ ] Code [Hamza]: Add `ContextStrategyResult` model (fragments, stats, errors) with deterministic ordering. @@ -3050,6 +3101,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Code [Hamza]: Add registry validation that strategies declare supported resource types. - [ ] Docs [Hamza]: Add `docs/reference/context_strategies.md` with strategy contracts and outputs. - [ ] Docs [Hamza]: Document strategy config keys (timeouts, max fragments, enable/disable). + - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Hamza]: Add `features/context_strategy_registry.feature` for registration and selection. - [ ] Tests (Robot) [Hamza]: Add `robot/context_strategy_registry.robot` smoke tests. - [ ] Tests (ASV) [Hamza]: Add `benchmarks/context_strategy_bench.py` for registry lookup. @@ -3060,11 +3112,10 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Hamza]: `git push -u origin feature/m6-acms-strategy-registry` - [ ] Forgejo PR [Hamza]: Open PR from `feature/m6-acms-strategy-registry` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Jeff | Group: ACMS4.fusion | Branch: feature/m6-acms-fusion-engine | Planned: Day 28 | Expected: Day 31) - Commit message: "feat(acms): add strategy coordinator and fusion engine"** +- [ ] **COMMIT (Owner: Jeff | Group: ACMS4.fusion | Branch: feature/m6-acms-fusion-engine | Planned: Day 28 | Expected: Day 35) - Commit message: "feat(acms): add strategy coordinator and fusion engine"** - [ ] Git [Jeff]: `git checkout master` - [ ] Git [Jeff]: `git pull origin master` - [ ] Git [Jeff]: `git checkout -b feature/m6-acms-fusion-engine` - - [ ] Git [Jeff]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Jeff]: Implement StrategyCoordinator with parallel execution and budget allocation. - [ ] Code [Jeff]: Implement FusionEngine to dedupe fragments, resolve detail conflicts, and pack within budget. - [ ] Code [Jeff]: Allocate budget proportionally by strategy confidence and enforce per-strategy max caps. @@ -3073,6 +3124,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Code [Jeff]: Add budget overage guard to drop lowest-relevance fragments and emit warnings. - [ ] Docs [Jeff]: Add `docs/reference/acms_fusion.md` with flow diagrams and budget semantics. - [ ] Docs [Jeff]: Document StrategyCoordinator execution order and error handling. + - [ ] Git [Jeff]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Jeff]: Add `features/acms_fusion.feature` for dedupe and budget enforcement. - [ ] Tests (Robot) [Jeff]: Add `robot/acms_fusion.robot` integration smoke tests. - [ ] Tests (ASV) [Jeff]: Add `benchmarks/acms_fusion_bench.py` for fusion runtime. @@ -3083,11 +3135,10 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Jeff]: `git push -u origin feature/m6-acms-fusion-engine` - [ ] Forgejo PR [Jeff]: Open PR from `feature/m6-acms-fusion-engine` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Hamza | Group: ACMS5.scoped | Branch: feature/m6-acms-scoped-view | Planned: Day 28 | Expected: Day 31) - Commit message: "feat(acms): add scoped backend view filtering"** +- [ ] **COMMIT (Owner: Hamza | Group: ACMS5.scoped | Branch: feature/m6-acms-scoped-view | Planned: Day 28 | Expected: Day 35) - Commit message: "feat(acms): add scoped backend view filtering"** - [ ] Git [Hamza]: `git checkout master` - [ ] Git [Hamza]: `git pull origin master` - [ ] Git [Hamza]: `git checkout -b feature/m6-acms-scoped-view` - - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Hamza]: Implement ScopedBackendView that filters text/vector/graph queries to project resources. - [ ] Code [Hamza]: Add enforcement hooks in context retrieval services and StrategyCoordinator. - [ ] Code [Hamza]: Add allowlist/denylist resolution for resource scopes using project resource links and aliases. @@ -3096,6 +3147,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Code [Hamza]: Add guard for mixed project scopes (explicit error when request spans unlinked projects). - [ ] Docs [Hamza]: Add `docs/reference/scoped_backend_view.md` with security guarantees. - [ ] Docs [Hamza]: Add examples of allowlist/denylist configurations. + - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Hamza]: Add `features/scoped_view.feature` for cross-project isolation. - [ ] Tests (Robot) [Hamza]: Add `robot/scoped_view.robot` smoke tests. - [ ] Tests (ASV) [Hamza]: Add `benchmarks/scoped_view_bench.py` for filter overhead. @@ -3106,11 +3158,10 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Hamza]: `git push -u origin feature/m6-acms-scoped-view` - [ ] Forgejo PR [Hamza]: Open PR from `feature/m6-acms-scoped-view` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Jeff | Group: ACMS6.skeleton | Branch: feature/m6-acms-skeleton-compress | Planned: Day 29 | Expected: Day 31) - Commit message: "feat(acms): add skeleton compressor"** +- [ ] **COMMIT (Owner: Jeff | Group: ACMS6.skeleton | Branch: feature/m6-acms-skeleton-compress | Planned: Day 29 | Expected: Day 35) - Commit message: "feat(acms): add skeleton compressor"** - [ ] Git [Jeff]: `git checkout master` - [ ] Git [Jeff]: `git pull origin master` - [ ] Git [Jeff]: `git checkout -b feature/m6-acms-skeleton-compress` - - [ ] Git [Jeff]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Jeff]: Implement skeleton compressor that produces compressed inherited context with `skeleton_ratio`. - [ ] Code [Jeff]: Integrate skeleton output into subplan context inheritance and strategy coordinator budgets. - [ ] Code [Jeff]: Add stable ordering for compressed fragments to avoid non-deterministic context payloads. @@ -3119,6 +3170,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Code [Jeff]: Add compression summary (original tokens vs compressed tokens) stored in plan metadata. - [ ] Docs [Jeff]: Add `docs/reference/skeleton_compressor.md` with ratios and examples. - [ ] Docs [Jeff]: Add example of skeleton output for a multi-decision plan. + - [ ] Git [Jeff]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Jeff]: Add `features/skeleton_compressor.feature` for compression thresholds. - [ ] Tests (Robot) [Jeff]: Add `robot/skeleton_compressor.robot` smoke tests. - [ ] Tests (ASV) [Jeff]: Add `benchmarks/skeleton_compressor_bench.py` for compression overhead. @@ -3129,7 +3181,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Jeff]: `git push -u origin feature/m6-acms-skeleton-compress` - [ ] Forgejo PR [Jeff]: Open PR from `feature/m6-acms-skeleton-compress` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Hamza | Group: M5.2.context-indexing | Branch: feature/m4-context-indexing | Planned: Day 27 | Expected: Day 28) - Commit message: "feat(context): add repo indexing service"** +- [ ] **COMMIT (Owner: Hamza | Group: M5.2.context-indexing | Branch: feature/m4-context-indexing | Planned: Day 27 | Expected: Day 33) - Commit message: "feat(context): add repo indexing service"** - [ ] Git [Hamza]: `git checkout master` - [ ] Git [Hamza]: `git pull origin master` - [ ] Git [Hamza]: `git checkout -b feature/m4-context-indexing` @@ -3167,15 +3219,15 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or **Parallel Group M5c: Subplan Expansion (Spawn Tooling + Service + Multi-Project)** -- [ ] **COMMIT (Owner: Jeff | Group: M5c.subplan-service | Branch: feature/m5-subplan-service | Planned: Day 26 | Expected: Day 28) - Commit message: "feat(service): add subplan service and spawn workflow"** +- [ ] **COMMIT (Owner: Jeff | Group: M5c.subplan-service | Branch: feature/m5-subplan-service | Planned: Day 26 | Expected: Day 34) - Commit message: "feat(service): add subplan service and spawn workflow"** - [ ] Git [Jeff]: `git checkout master` - [ ] Git [Jeff]: `git pull origin master` - [ ] Git [Jeff]: `git checkout -b feature/m5-subplan-service` - - [ ] Git [Jeff]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Jeff]: Add subplan service that builds child plans from DecisionService spawn entries and SubplanConfig. - [ ] Code [Jeff]: Persist subplan spawn metadata (spawn_decision_id, parent/root plan ids, execution mode) for status output. - [ ] Code [Jeff]: Add subplan spawn validation (resource scopes resolved, merge strategy defined, max_parallel bounds). - [ ] Docs [Jeff]: Add `docs/reference/subplan_service.md` with spawn workflow and lifecycle. + - [ ] Git [Jeff]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Jeff]: Add scenarios for spawn workflow and invalid spawn payloads. - [ ] Tests (Robot) [Jeff]: Add subplan spawn integration smoke tests. - [ ] Tests (ASV) [Jeff]: Add `benchmarks/subplan_spawn_bench.py` for spawn throughput. @@ -3186,11 +3238,10 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Jeff]: `git push -u origin feature/m5-subplan-service` - [ ] Forgejo PR [Jeff]: Open PR from `feature/m5-subplan-service` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Aditya | Group: M5c.subplan-actor | Branch: feature/m5-subplan-actor | Planned: Day 26 | Expected: Day 28) - Commit message: "feat(actor): add plan_subplan tool and decision emission"** +- [ ] **COMMIT (Owner: Aditya | Group: M5c.subplan-actor | Branch: feature/m5-subplan-actor | Planned: Day 26 | Expected: Day 34) - Commit message: "feat(actor): add plan_subplan tool and decision emission"** - [ ] Git [Aditya]: `git checkout master` - [ ] Git [Aditya]: `git pull origin master` - [ ] Git [Aditya]: `git checkout -b feature/m5-subplan-actor` - - [ ] Git [Aditya]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Aditya]: Add `plan_subplan` tool to strategy actors and emit SUBPLAN_SPAWN decisions. - [ ] Code [Aditya]: Support `parallel=true` to emit SUBPLAN_PARALLEL_SPAWN and include dependency list. - [ ] Code [Aditya]: Include merge strategy, resource scope, and context view overrides in decision payload. @@ -3200,6 +3251,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Code [Aditya]: Emit decision rationale text with summary of why subplan was spawned (for explain output). - [ ] Docs [Aditya]: Update actor YAML examples for subplan emission. - [ ] Docs [Aditya]: Add a minimal `plan_subplan` tool payload example and a parallel spawn example with dependencies. + - [ ] Git [Aditya]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Aditya]: Add scenarios for subplan decision emission (parallel + dependencies). - [ ] Tests (Robot) [Aditya]: Add actor tool integration smoke tests. - [ ] Tests (ASV) [Aditya]: Add `benchmarks/subplan_actor_tool_bench.py` for tool invocation overhead. @@ -3210,11 +3262,10 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Aditya]: `git push -u origin feature/m5-subplan-actor` - [ ] Forgejo PR [Aditya]: Open PR from `feature/m5-subplan-actor` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Hamza | Group: M5c.multi-project | Branch: feature/m5-multi-project | Planned: Day 29 | Expected: Day 28) - Commit message: "feat(plan): add multi-project subplan support"** +- [ ] **COMMIT (Owner: Hamza | Group: M5c.multi-project | Branch: feature/m5-multi-project | Planned: Day 29 | Expected: Day 34) - Commit message: "feat(plan): add multi-project subplan support"** - [ ] Git [Hamza]: `git checkout master` - [ ] Git [Hamza]: `git pull origin master` - [ ] Git [Hamza]: `git checkout -b feature/m5-multi-project` - - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Hamza]: Allow plans to target multiple projects with separate resource link contexts. - [ ] Code [Hamza]: Ensure sandbox isolation and cross-project dependency resolution. - [ ] Code [Hamza]: Add plan metadata to track project-specific ChangeSets and validation summaries. @@ -3224,6 +3275,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Code [Hamza]: Enforce that a plan cannot mix read-only and write-capable projects without explicit override. - [ ] Docs [Hamza]: Add `docs/reference/multi_project_plans.md`. - [ ] Docs [Hamza]: Add examples for multi-project `plan use` and explain alias resolution. + - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Hamza]: Add multi-project subplan scenarios. - [ ] Tests (Robot) [Hamza]: Add multi-project integration tests. - [ ] Tests (ASV) [Hamza]: Add `benchmarks/multi_project_bench.py` for multi-project overhead. @@ -3234,7 +3286,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Hamza]: `git push -u origin feature/m5-multi-project` - [ ] Forgejo PR [Hamza]: Open PR from `feature/m5-multi-project` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Brent | Group: M5.tests | Branch: feature/m5-acms-smoke | Planned: Day 28 | Expected: Day 28) - Commit message: "test(e2e): add M5 ACMS + context suites"** +- [ ] **COMMIT (Owner: Brent | Group: M5.tests | Branch: feature/m5-acms-smoke | Planned: Day 28 | Expected: Day 34) - Commit message: "test(e2e): add M5 ACMS + context suites"** - [ ] Git [Brent]: `git checkout master` - [ ] Git [Brent]: `git pull origin master` - [ ] Git [Brent]: `git checkout -b feature/m5-acms-smoke` @@ -3254,7 +3306,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or #### M6: Autonomy Hardening + Server Stubs (Day 30) **Parallel Group M6: ACP Stubs + Autonomy Guardrails + Final Acceptance** -- [ ] **COMMIT (Owner: Luis | Group: M6.1.server-stubs | Branch: feature/m6-server-stubs | Planned: Day 29 | Expected: Day 30) - Commit message: "feat(interfaces): add server client stubs"** +- [ ] **COMMIT (Owner: Luis | Group: M6.1.server-stubs | Branch: feature/m6-server-stubs | Planned: Day 29 | Expected: Day 35) - Commit message: "feat(interfaces): add server client stubs"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m6-server-stubs` @@ -3292,15 +3344,15 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [X] Git [Jeff]: `git push -u origin feature/m6-acp-stubs` - [X] Forgejo PR [Jeff]: Open PR from `feature/m6-acp-stubs` to `master` with a suitable and thorough description (PR #148) -- [ ] **COMMIT (Owner: Jeff | Group: M6.4.lsp-stub | Branch: feature/m6-lsp-stub | Planned: Day 29 | Expected: Day 31) - Commit message: "feat(lsp): add LSP server stub"** +- [ ] **COMMIT (Owner: Jeff | Group: M6.4.lsp-stub | Branch: feature/m6-lsp-stub | Planned: Day 29 | Expected: Day 35) - Commit message: "feat(lsp): add LSP server stub"** - [ ] Git [Jeff]: `git checkout master` - [ ] Git [Jeff]: `git pull origin master` - [ ] Git [Jeff]: `git checkout -b feature/m6-lsp-stub` - - [ ] Git [Jeff]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Jeff]: Add minimal LSP server entrypoint that supports initialize/shutdown and reports stubbed capability set. - [ ] Code [Jeff]: Wire LSP requests to ACP facade (local mode) and return explicit "not implemented" responses for unsupported methods. - [ ] Code [Jeff]: Add CLI command `agents lsp` to launch the stub server with logging and PID output. - [ ] Docs [Jeff]: Add `docs/reference/lsp_stub.md` with usage notes and supported methods. + - [ ] Git [Jeff]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Jeff]: Add scenarios for LSP initialize/shutdown handshake and stub error responses. - [ ] Tests (Robot) [Jeff]: Add Robot smoke test that launches the LSP stub and validates startup banner. - [ ] Tests (ASV) [Jeff]: Add `benchmarks/lsp_stub_bench.py` for startup latency. @@ -3311,7 +3363,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Jeff]: `git push -u origin feature/m6-lsp-stub` - [ ] Forgejo PR [Jeff]: Open PR from `feature/m6-lsp-stub` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Luis | Group: M6.3.autonomy-guards | Branch: feature/m6-autonomy-guards | Planned: Day 29 | Expected: Day 30) - Commit message: "feat(automation): add autonomy guardrails and audit trail"** +- [ ] **COMMIT (Owner: Luis | Group: M6.3.autonomy-guards | Branch: feature/m6-autonomy-guards | Planned: Day 29 | Expected: Day 35) - Commit message: "feat(automation): add autonomy guardrails and audit trail"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m6-autonomy-guards` @@ -3330,11 +3382,10 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or **Parallel Group M6b: Large Project Autonomy (Decomposition + Checkpoints + Semantic Validation + Context Tiers + Estimation + CLI Polish)** -- [ ] **COMMIT (Owner: Jeff | Group: G1.decompose | Branch: feature/m6-large-decompose | Planned: Day 26 | Expected: Day 31) - Commit message: "feat(plan): add large-project decomposition and dependency closure"** +- [ ] **COMMIT (Owner: Jeff | Group: G1.decompose | Branch: feature/m6-large-decompose | Planned: Day 26 | Expected: Day 35) - Commit message: "feat(plan): add large-project decomposition and dependency closure"** - [ ] Git [Jeff]: `git checkout master` - [ ] Git [Jeff]: `git pull origin master` - [ ] Git [Jeff]: `git checkout -b feature/m6-large-decompose` - - [ ] Git [Jeff]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Jeff]: Add hierarchical decomposition with 4+ levels and bounded context per subplan. - [ ] Code [Jeff]: Implement decomposition heuristics (max_files_per_subplan, max_tokens_per_subplan, language/dir clustering). - [ ] Code [Jeff]: Add dependency closure computation for large graphs and DAG execution ordering. @@ -3347,6 +3398,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Code [Jeff]: Add guard to avoid spawning subplans when project is below threshold; default to single plan. - [ ] Docs [Jeff]: Add `docs/reference/large_project_decomposition.md`. - [ ] Docs [Jeff]: Document config keys, default thresholds, and example decomposition outputs. + - [ ] Git [Jeff]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Jeff]: Add deep hierarchy + dependency closure scenarios. - [ ] Tests (Robot) [Jeff]: Add large-project decomposition integration tests. - [ ] Tests (ASV) [Jeff]: Add `benchmarks/large_project_decompose_bench.py` for decomposition runtime. @@ -3357,11 +3409,10 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Jeff]: `git push -u origin feature/m6-large-decompose` - [ ] Forgejo PR [Jeff]: Open PR from `feature/m6-large-decompose` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Luis | Group: G2.checkpoint | Branch: feature/m6-checkpoint | Planned: Day 27 | Expected: Day 31) - Commit message: "feat(checkpoint): add checkpointing and rollback"** +- [ ] **COMMIT (Owner: Luis | Group: G2.checkpoint | Branch: feature/m6-checkpoint | Planned: Day 27 | Expected: Day 35) - Commit message: "feat(checkpoint): add checkpointing and rollback"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m6-checkpoint` - - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Luis]: Add checkpoint declarations for tools and plan-level rollback policy. - [ ] Code [Luis]: Add `checkpoints` table (checkpoint_id ULID, plan_id, sandbox_ref, created_at, metadata_json). - [ ] Code [Luis]: Implement `plan rollback ` command. @@ -3373,6 +3424,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Code [Luis]: Add guard preventing rollback when plan is applied or sandbox is missing. - [ ] Docs [Luis]: Add `docs/reference/checkpointing.md`. - [ ] Docs [Luis]: Include checkpoint retention defaults and rollback error cases. + - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Luis]: Add checkpoint/rollback scenarios. - [ ] Tests (Robot) [Luis]: Add rollback integration tests. - [ ] Tests (ASV) [Luis]: Add `benchmarks/checkpoint_rollback_bench.py` for rollback latency. @@ -3383,11 +3435,10 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Luis]: `git push -u origin feature/m6-checkpoint` - [ ] Forgejo PR [Luis]: Open PR from `feature/m6-checkpoint` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Luis | Group: G3.semantic | Branch: feature/m6-semantic-validation | Planned: Day 27 | Expected: Day 31) - Commit message: "feat(validation): add semantic validation service"** +- [ ] **COMMIT (Owner: Luis | Group: G3.semantic | Branch: feature/m6-semantic-validation | Planned: Day 27 | Expected: Day 35) - Commit message: "feat(validation): add semantic validation service"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m6-semantic-validation` - - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Luis]: Add semantic validation hooks during strategize/execute and error-pattern checks. - [ ] Code [Luis]: Add built-in semantic checks for syntax errors, missing imports, and broken references for Python projects. - [ ] Code [Luis]: Expose semantic validation as Validation tools so they can be attached per resource. @@ -3399,6 +3450,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Code [Luis]: Add caching for semantic checks keyed by file hash to avoid rework on unchanged files. - [ ] Docs [Luis]: Add `docs/reference/semantic_validation.md`. - [ ] Docs [Luis]: Add section on required vs informational validation attachment modes. + - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Luis]: Add semantic validation scenarios. - [ ] Tests (Robot) [Luis]: Add semantic validation integration tests. - [ ] Tests (ASV) [Luis]: Add `benchmarks/semantic_validation_bench.py` for validation cost. @@ -3409,11 +3461,10 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Luis]: `git push -u origin feature/m6-semantic-validation` - [ ] Forgejo PR [Luis]: Open PR from `feature/m6-semantic-validation` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Hamza | Group: G4.context | Branch: feature/m6-context-tiers | Planned: Day 28 | Expected: Day 31) - Commit message: "feat(context): add hot/warm/cold tiers and actor views"** +- [ ] **COMMIT (Owner: Hamza | Group: G4.context | Branch: feature/m6-context-tiers | Planned: Day 28 | Expected: Day 35) - Commit message: "feat(context): add hot/warm/cold tiers and actor views"** - [ ] Git [Hamza]: `git checkout master` - [ ] Git [Hamza]: `git pull origin master` - [ ] Git [Hamza]: `git checkout -b feature/m6-context-tiers` - - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Hamza]: Implement hot/warm/cold tiers with indexing, LRU eviction, and promotion/demotion. - [ ] Code [Hamza]: Add tier storage backends (in-memory hot, sqlite warm, file-backed cold). - [ ] Code [Hamza]: Add per-actor context views (strategist/executor/reviewer) and filtered presentation. @@ -3426,6 +3477,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Code [Hamza]: Add metrics for tier hit/miss counts and expose via `agents diagnostics`. - [ ] Docs [Hamza]: Add `docs/reference/context_tiers.md`. - [ ] Docs [Hamza]: Document tier budgets, eviction rules, and summarization policy. + - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Hamza]: Add context tier scenarios. - [ ] Tests (Robot) [Hamza]: Add context tier integration tests. - [ ] Tests (ASV) [Hamza]: Add `benchmarks/context_tiers_bench.py` for tier lookup performance. @@ -3436,11 +3488,10 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Hamza]: `git push -u origin feature/m6-context-tiers` - [ ] Forgejo PR [Hamza]: Open PR from `feature/m6-context-tiers` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Hamza | Group: G5.estimate | Branch: feature/m6-estimation | Planned: Day 28 | Expected: Day 31) - Commit message: "feat(estimation): add cost and risk estimation actor"** +- [ ] **COMMIT (Owner: Hamza | Group: G5.estimate | Branch: feature/m6-estimation | Planned: Day 28 | Expected: Day 35) - Commit message: "feat(estimation): add cost and risk estimation actor"** - [ ] Git [Hamza]: `git checkout master` - [ ] Git [Hamza]: `git pull origin master` - [ ] Git [Hamza]: `git checkout -b feature/m6-estimation` - - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Hamza]: Add optional `estimation_actor` role and cost/risk estimation outputs. - [ ] Code [Hamza]: Persist estimation output to plan metadata (cost_estimate, risk_score, duration_estimate). - [ ] Code [Hamza]: Invoke estimation during `plan use` and surface estimates in `plan status` output. @@ -3449,6 +3500,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Code [Hamza]: Add error handling when estimation actor fails (fallback to informational warning). - [ ] Docs [Hamza]: Add `docs/reference/estimation.md` with output format. - [ ] Docs [Hamza]: Add CLI examples showing estimate fields in `plan status` output. + - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Hamza]: Add estimation scenarios. - [ ] Tests (Robot) [Hamza]: Add estimation integration smoke tests. - [ ] Tests (ASV) [Hamza]: Add `benchmarks/estimation_actor_bench.py` for estimation runtime. @@ -3459,11 +3511,10 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Hamza]: `git push -u origin feature/m6-estimation` - [ ] Forgejo PR [Hamza]: Open PR from `feature/m6-estimation` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Jeff | Group: G6.cli | Branch: feature/m6-cli-polish | Planned: Day 29 | Expected: Day 31) - Commit message: "chore(cli): polish help and output"** +- [ ] **COMMIT (Owner: Jeff | Group: G6.cli | Branch: feature/m6-cli-polish | Planned: Day 29 | Expected: Day 35) - Commit message: "chore(cli): polish help and output"** - [ ] Git [Jeff]: `git checkout master` - [ ] Git [Jeff]: `git pull origin master` - [ ] Git [Jeff]: `git checkout -b feature/m6-cli-polish` - - [ ] Git [Jeff]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Jeff]: Standardize help text, progress indicators, and error messages with recovery hints. - [ ] Code [Jeff]: Ensure `--format` outputs are consistent (rich/color/table/plain/json/yaml) across core commands. - [ ] Code [Jeff]: Ensure `plain` format uses ASCII-only output to support log pipelines. @@ -3472,6 +3523,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Code [Jeff]: Add unified error envelope for JSON/YAML outputs (`error.code`, `error.message`, `error.details`). - [ ] Docs [Jeff]: Update CLI output examples where needed. - [ ] Docs [Jeff]: Add a CLI output contract section in `docs/reference/cli_output.md`. + - [ ] Git [Jeff]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Jeff]: Add `features/cli_output_formats.feature` covering rich/plain/json/yaml formatting for core commands. - [ ] Tests (Robot) [Jeff]: Add CLI UX smoke tests for critical commands. - [ ] Tests (ASV) [Jeff]: Add `benchmarks/cli_render_bench.py` for output rendering overhead. @@ -3482,7 +3534,7 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or - [ ] Git [Jeff]: `git push -u origin feature/m6-cli-polish` - [ ] Forgejo PR [Jeff]: Open PR from `feature/m6-cli-polish` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Brent | Group: M6.tests | Branch: feature/m6-autonomy-smoke | Planned: Day 30 | Expected: Day 30) - Commit message: "test(e2e): add M6 autonomy acceptance suite"** +- [ ] **COMMIT (Owner: Brent | Group: M6.tests | Branch: feature/m6-autonomy-smoke | Planned: Day 30 | Expected: Day 35) - Commit message: "test(e2e): add M6 autonomy acceptance suite"** - [ ] Git [Brent]: `git checkout master` - [ ] Git [Brent]: `git pull origin master` - [ ] Git [Brent]: `git checkout -b feature/m6-autonomy-smoke` @@ -5834,11 +5886,10 @@ By Day 30, the system must be able to (all in LOCAL MODE, server connectivity is **Parallel Group 10A: Async Infrastructure [Luis]** -- [ ] **COMMIT (Owner: Luis | Group: 10A.async | Branch: feature/m6-async-infra | Planned: Day 27 | Expected: Day 36) - Commit message: "feat(async): add async command execution and workers"** +- [ ] **COMMIT (Owner: Luis | Group: 10A.async | Branch: feature/m6-async-infra | Planned: Day 27 | Expected: Day 41) - Commit message: "feat(async): add async command execution and workers"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m6-async-infra` - - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Luis]: Implement async command execution per ADR-002 with cancellation and timeout handling. - [ ] Code [Luis]: Add `AsyncJob` model and `async_jobs` table (plan_id, phase, status, payload_json, created_at, started_at, finished_at). - [ ] Code [Luis]: Add `AsyncJobStatus` enum and enforce valid transitions (queued -> running -> succeeded/failed/cancelled). @@ -5852,6 +5903,7 @@ By Day 30, the system must be able to (all in LOCAL MODE, server connectivity is - [ ] Code [Luis]: Add serialization of async payloads with schema version for forward compatibility. - [ ] Docs [Luis]: Update `docs/reference/async_architecture.md` with execution flow, job states, and shutdown rules. - [ ] Docs [Luis]: Document async config defaults and job retention policy. + - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Luis]: Add `features/async_execution.feature` for async command handling (enqueue, worker pick-up, cancel). - [ ] Tests (Robot) [Luis]: Add `robot/async_execution.robot` smoke tests. - [ ] Tests (ASV) [Luis]: Add `benchmarks/async_execution_bench.py` for worker scheduling overhead. @@ -5861,11 +5913,10 @@ By Day 30, the system must be able to (all in LOCAL MODE, server connectivity is - [ ] Git [Luis]: `git commit -m "feat(async): add async command execution and workers"` - [ ] Git [Luis]: `git push -u origin feature/m6-async-infra` - [ ] Forgejo PR [Luis]: Open PR from `feature/m6-async-infra` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Luis | Group: 10A.retry | Branch: feature/m6-async-infra | Planned: Day 28 | Expected: Day 37) - Commit message: "feat(async): wire retry policies into services"** +- [ ] **COMMIT (Owner: Luis | Group: 10A.retry | Branch: feature/m6-async-infra | Planned: Day 28 | Expected: Day 42) - Commit message: "feat(async): wire retry policies into services"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m6-async-infra` - - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Luis]: Integrate retry/circuit breaker policies into service layer operations. - [ ] Code [Luis]: Add retry policy configuration keys (max_attempts, base_delay, max_delay, jitter) to settings. - [ ] Code [Luis]: Ensure retries are only applied to idempotent operations (repository reads, validation calls) and never to applies. @@ -5876,6 +5927,7 @@ By Day 30, the system must be able to (all in LOCAL MODE, server connectivity is - [ ] Code [Luis]: Add circuit breaker half-open recovery logic and cooldown timers. - [ ] Docs [Luis]: Document retry policy defaults and override points. - [ ] Docs [Luis]: Add examples of per-service override config and expected logs. + - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Luis]: Add retry/circuit breaker behavior scenarios. - [ ] Tests (Robot) [Luis]: Add resilience smoke tests. - [ ] Tests (ASV) [Luis]: Add `benchmarks/retry_policy_bench.py` for retry overhead. @@ -5930,16 +5982,16 @@ By Day 30, the system must be able to (all in LOCAL MODE, server connectivity is - [X] Quality [Brent]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. — verified 97% on develop-brent-2 (4437 scenarios, 515 robot tests) on 2026-02-20 - [X] Quality [Brent]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it. — all nox sessions pass on develop-brent-2 (lint, typecheck, unit_tests, integration_tests, coverage_report) on 2026-02-20 - [X] Forgejo PR [Brent]: Open PR from `feature/m6-validation-edge` to `master` with a suitable and thorough description. — merged to master via develop-brent-2 PR #132 on 2026-02-20 -- [ ] **COMMIT (Owner: Luis | Group: 10C.semantic | Branch: feature/m6-validation-semantic | Planned: Day 25 | Expected: Day 31) - Commit message: "test(validation): add semantic validation suites"** +- [ ] **COMMIT (Owner: Luis | Group: 10C.semantic | Branch: feature/m6-validation-semantic | Planned: Day 25 | Expected: Day 36) - Commit message: "test(validation): add semantic validation suites"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m6-validation-semantic` - - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Luis]: Add semantic validation fixtures and error-pattern samples. - [ ] Code [Luis]: Add fixtures for language-porting mismatches and dependency graph violations. - [ ] Code [Luis]: Add fixtures for API surface changes (renamed functions, missing symbols, incompatible types). - [ ] Code [Luis]: Add fixtures for cross-file symbol resolution and circular import detection. - [ ] Docs [Luis]: Document semantic validation coverage expectations. + - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Luis]: Add semantic validation scenarios. - [ ] Tests (Robot) [Luis]: Add semantic validation integration tests. - [ ] Tests (ASV) [Luis]: Add `benchmarks/semantic_validation_suite_bench.py` for suite runtime. @@ -6000,17 +6052,17 @@ By Day 30, the system must be able to (all in LOCAL MODE, server connectivity is - [ ] Forgejo PR [Luis]: Open PR from `feature/m4-security-eval` to `master` with description "Remove eval-based config parsing and add security checks.". (Code review by Brent still pending) **Parallel Group SEC2: Template Injection Prevention [Luis]** -- [ ] **COMMIT (Owner: Luis | Group: SEC2.template | Branch: feature/m4-security-template | Planned: Day 11 | Expected: Day 26) - Commit message: "fix(security): harden template rendering"** +- [ ] **COMMIT (Owner: Luis | Group: SEC2.template | Branch: feature/m4-security-template | Planned: Day 11 | Expected: Day 35) - Commit message: "fix(security): harden template rendering"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m4-security-template` - - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Luis]: Replace unsafe template usage with a sandboxed renderer and strict token set. - [ ] Code [Luis]: Deny attribute access, function calls, and filters; allow only `{var}` substitution with a fixed allowlist. - [ ] Code [Luis]: Add max template length + max render output size checks with explicit errors. - [ ] Code [Luis]: Add template allowlist validation for each template context key and log rejected keys. - [ ] Code [Luis]: Add unit helper to pre-validate template strings at action/plan creation time. - [ ] Docs [Luis]: Add `docs/reference/template_security.md` with safe patterns. + - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Luis]: Add `features/security_templates.feature` scenarios. - [ ] Tests (Robot) [Luis]: Add `robot/security_templates.robot` smoke tests. - [ ] Tests (ASV) [Luis]: Add `benchmarks/security_template_bench.py` for render baseline. @@ -6022,17 +6074,17 @@ By Day 30, the system must be able to (all in LOCAL MODE, server connectivity is - [ ] Forgejo PR [Luis]: Open PR from `feature/m4-security-template` to `master` with a suitable and thorough description **Parallel Group SEC3: Exception Handling Audit [Luis]** -- [ ] **COMMIT (Owner: Luis | Group: SEC3.exceptions | Branch: feature/m4-security-exceptions | Planned: Day 12 | Expected: Day 26) - Commit message: "fix(security): enforce explicit exception handling"** +- [ ] **COMMIT (Owner: Luis | Group: SEC3.exceptions | Branch: feature/m4-security-exceptions | Planned: Day 12 | Expected: Day 35) - Commit message: "fix(security): enforce explicit exception handling"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m4-security-exceptions` - - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Luis]: Replace silent exception handling with explicit errors and context propagation. - [ ] Code [Luis]: Add structured error types for config, provider, and file I/O failures and include plan_id/project_name in error details. - [ ] Code [Luis]: Ensure unexpected exceptions are wrapped in `CleverAgentsError` with a safe, user-facing message. - [ ] Code [Luis]: Add error code mapping table (error_code -> HTTP-like category) for CLI output consistency. - [ ] Code [Luis]: Ensure error details are redacted for secrets before logging. - [ ] Docs [Luis]: Document error propagation standards and logging rules. + - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Luis]: Add `features/security_exceptions.feature` scenarios. - [ ] Tests (Robot) [Luis]: Add exception handling integration smoke tests. - [ ] Tests (ASV) [Luis]: Add `benchmarks/security_exception_bench.py` for error path overhead baseline. @@ -6044,17 +6096,17 @@ By Day 30, the system must be able to (all in LOCAL MODE, server connectivity is - [ ] Forgejo PR [Luis]: Open PR from `feature/m4-security-exceptions` to `master` with a suitable and thorough description **Parallel Group SEC4: Async Lifecycle Correctness [Luis]** -- [ ] **COMMIT (Owner: Luis | Group: SEC4.async | Branch: feature/m4-security-async-cleanup | Planned: Day 12 | Expected: Day 26) - Commit message: "fix(security): close async resources and leaks"** +- [ ] **COMMIT (Owner: Luis | Group: SEC4.async | Branch: feature/m4-security-async-cleanup | Planned: Day 12 | Expected: Day 35) - Commit message: "fix(security): close async resources and leaks"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m4-security-async-cleanup` - - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Luis]: Close async resources, checkpoint files, and subscription leaks with retention policies. - [ ] Code [Luis]: Add graceful cancellation handling to ensure in-flight tasks are awaited and cleaned up. - [ ] Code [Luis]: Add a finalizer hook in async services that logs any leaked resources by name. - [ ] Code [Luis]: Add cleanup for pending async jobs on shutdown (mark cancelled and persist reason). - [ ] Code [Luis]: Add time-bounded shutdown sequence with explicit warnings for forced termination. - [ ] Docs [Luis]: Add `docs/reference/async_safety.md` on cleanup rules. + - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Luis]: Add `features/security_async.feature` scenarios. - [ ] Tests (Robot) [Luis]: Add async cleanup integration tests. - [ ] Tests (ASV) [Luis]: Add `benchmarks/security_async_cleanup_bench.py` for cleanup overhead baseline. @@ -6090,17 +6142,17 @@ By Day 30, the system must be able to (all in LOCAL MODE, server connectivity is - [x] Git [Hamza]: Branch kept for PR review (not deleted yet) **Parallel Group SEC6: Read-Only Enforcement [Luis]** -- [ ] **COMMIT (Owner: Luis | Group: SEC6.readonly | Branch: feature/m4-security-readonly | Planned: Day 13 | Expected: Day 26) - Commit message: "feat(security): enforce read-only actions"** +- [ ] **COMMIT (Owner: Luis | Group: SEC6.readonly | Branch: feature/m4-security-readonly | Planned: Day 13 | Expected: Day 35) - Commit message: "feat(security): enforce read-only actions"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m4-security-readonly` - - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Luis]: Validate read-only actions only use read-only skills at execution time. - [ ] Code [Luis]: Block write-capable tools in ToolRuntime when plan/action is read-only and include tool name in error. - [ ] Code [Luis]: Add read-only enforcement to SkillContext and ChangeSet builder to prevent write artifacts. - [ ] Code [Luis]: Add read-only enforcement in CLI commands that would mutate resources (fail fast before execution). - [ ] Code [Luis]: Add tests for read-only enforcement on file and git tool calls. - [ ] Docs [Luis]: Add `docs/reference/read_only_actions.md`. + - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Luis]: Add `features/security_readonly.feature` scenarios. - [ ] Tests (Robot) [Luis]: Add read-only enforcement integration tests. - [ ] Tests (ASV) [Luis]: Add `benchmarks/security_readonly_bench.py` for enforcement overhead baseline. @@ -6141,11 +6193,10 @@ By Day 30, the system must be able to (all in LOCAL MODE, server connectivity is **Target: Days 8-12** **Parallel Group PROV1: Provider Fixes [Luis]** -- [ ] **COMMIT (Owner: Luis | Group: PROV1.fixes | Branch: feature/m4-provider-fixes | Planned: Day 8 | Expected: Day 26) - Commit message: "fix(provider): remove FakeListLLM defaults"** +- [ ] **COMMIT (Owner: Luis | Group: PROV1.fixes | Branch: feature/m4-provider-fixes | Planned: Day 8 | Expected: Day 35) - Commit message: "fix(provider): remove FakeListLLM defaults"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m4-provider-fixes` - - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Luis]: Remove FakeListLLM fallback, fix auto-debug provider usage, and implement provider auto-detection. - [ ] Code [Luis]: Update settings validation to fail fast when no providers are configured and no mock flag is set. - [ ] Code [Luis]: Add explicit `core.mock_providers` flag and block accidental use in non-test mode. @@ -6154,6 +6205,7 @@ By Day 30, the system must be able to (all in LOCAL MODE, server connectivity is - [ ] Code [Luis]: Add unit helper to resolve provider by name and emit explicit error when not configured. - [ ] Docs [Luis]: Update provider configuration docs and error messages. - [ ] Docs [Luis]: Add migration note for removing FakeListLLM fallback and mock flag usage. + - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Luis]: Add `features/provider_fixes.feature` scenarios. - [ ] Tests (Robot) [Luis]: Add provider detection smoke tests. - [ ] Tests (ASV) [Luis]: Add `benchmarks/provider_selection_bench.py` for provider resolution baseline. @@ -6165,11 +6217,10 @@ By Day 30, the system must be able to (all in LOCAL MODE, server connectivity is - [ ] Forgejo PR [Luis]: Open PR from `feature/m4-provider-fixes` to `master` with a suitable and thorough description **Parallel Group PROV2: Cost Controls & Fallback [Luis]** -- [ ] **COMMIT (Owner: Luis | Group: PROV2.costs | Branch: feature/m4-provider-costs | Planned: Day 10 | Expected: Day 26) - Commit message: "feat(provider): add cost controls and fallback"** +- [ ] **COMMIT (Owner: Luis | Group: PROV2.costs | Branch: feature/m4-provider-costs | Planned: Day 10 | Expected: Day 35) - Commit message: "feat(provider): add cost controls and fallback"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m4-provider-costs` - - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Luis]: Track tokens/costs, enforce budgets, rate limits, and provider fallback order. - [ ] Code [Luis]: Add cost tracking fields to plan execution metadata and surface in `plan status`. - [ ] Code [Luis]: Add config keys for `budget_per_plan`, `budget_per_day`, and `fallback_providers` with validation. @@ -6178,6 +6229,7 @@ By Day 30, the system must be able to (all in LOCAL MODE, server connectivity is - [ ] Code [Luis]: Add fallback selection logic that skips providers without required capabilities (tool calling, streaming). - [ ] Code [Luis]: Persist budget exhaustion events in plan metadata for auditability. - [ ] Docs [Luis]: Add `docs/reference/cost_controls.md` with config keys and thresholds. + - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Luis]: Add `features/cost_controls.feature` scenarios. - [ ] Tests (Robot) [Luis]: Add cost control integration smoke tests. - [ ] Tests (ASV) [Luis]: Add `benchmarks/cost_controls_bench.py` for cost check overhead. @@ -6240,18 +6292,18 @@ By Day 30, the system must be able to (all in LOCAL MODE, server connectivity is - [X] Git [Jeff]: `git push -u origin feature/m4-cli-extensions` - [X] Forgejo PR [Jeff]: Open PR from `feature/m4-cli-extensions` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Brent | Group: CLI1.beta | Branch: feature/m4-cli-extension-tests | Planned: Day 12 | Expected: Day 26) - Commit message: "test(cli): cover action and plan extensions"** +- [ ] **COMMIT (Owner: Brent | Group: CLI1.beta | Branch: feature/m4-cli-extension-tests | Planned: Day 12 | Expected: Day 35) - Commit message: "test(cli): cover action and plan extensions"** - [ ] Git [Brent]: `git checkout master` - [ ] Git [Brent]: `git pull origin master` - [ ] Git [Brent]: `git checkout -b feature/m4-cli-extension-tests` + - [ ] Docs [Brent]: Update `docs/development/testing.md` with CLI extension fixtures. - [ ] Git [Brent]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Brent]: Add scenarios for automation_profile resolution, invariant ordering, and actor override errors. - [ ] Tests (Behave) [Brent]: Add output snapshot assertions for extended fields in JSON/YAML/table formats. - [ ] Tests (Robot) [Brent]: Add Robot test that validates `action show` includes optional actors/invariants when set. - - [ ] Docs [Brent]: Update `docs/development/testing.md` with CLI extension fixtures. - [ ] Tests (ASV) [Brent]: Add `benchmarks/cli_extension_tests_bench.py` for extended scenario runtime baseline. - [ ] Quality [Brent]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. - - [ ] Quality [Brent]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes. + - [ ] Quality [Brent]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it. - [ ] Git [Brent]: Perform an appropriate `git add` command to add all the files that should be part of this commit to the git index - [ ] Git [Brent]: `git commit -m "test(cli): cover action and plan extensions"` (run after the coverage check below passes) - [ ] Git [Brent]: `git push -u origin feature/m4-cli-extension-tests` @@ -6261,11 +6313,10 @@ By Day 30, the system must be able to (all in LOCAL MODE, server connectivity is ### Section 14: Concurrency & Cleanup [Days 12-14] **Parallel Group CONC1: Plan Locking [Luis]** -- [ ] **COMMIT (Owner: Luis | Group: CONC1.lock | Branch: feature/m4-concurrency-locks | Planned: Day 12 | Expected: Day 26) - Commit message: "feat(concurrency): add plan and project locks"** +- [ ] **COMMIT (Owner: Luis | Group: CONC1.lock | Branch: feature/m4-concurrency-locks | Planned: Day 12 | Expected: Day 35) - Commit message: "feat(concurrency): add plan and project locks"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m4-concurrency-locks` - - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Luis]: Implement plan-level and project-level locks with timeouts. - [ ] Code [Luis]: Add `locks` table with owner_id, resource_type, resource_id, acquired_at, expires_at. - [ ] Code [Luis]: Ensure locks are enforced in PlanLifecycleService transitions and SubplanService scheduling. @@ -6275,6 +6326,7 @@ By Day 30, the system must be able to (all in LOCAL MODE, server connectivity is - [ ] Code [Luis]: Add `agents diagnostics` check to report stale locks count. - [ ] Docs [Luis]: Add `docs/reference/concurrency.md` with lock behavior. - [ ] Docs [Luis]: Document lock TTL defaults and renewal strategy. + - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Luis]: Add `features/concurrency.feature` scenarios for lock contention and expiry. - [ ] Tests (Robot) [Luis]: Add lock integration smoke tests. - [ ] Tests (ASV) [Luis]: Add `benchmarks/concurrency_lock_bench.py` for lock overhead baseline. @@ -6286,11 +6338,10 @@ By Day 30, the system must be able to (all in LOCAL MODE, server connectivity is - [ ] Forgejo PR [Luis]: Open PR from `feature/m4-concurrency-locks` to `master` with a suitable and thorough description **Parallel Group CONC2: Resumable Execution [Luis]** -- [ ] **COMMIT (Owner: Luis | Group: CONC2.resume | Branch: feature/m4-concurrency-resume | Planned: Day 13 | Expected: Day 26) - Commit message: "feat(concurrency): add plan resume"** +- [ ] **COMMIT (Owner: Luis | Group: CONC2.resume | Branch: feature/m4-concurrency-resume | Planned: Day 13 | Expected: Day 35) - Commit message: "feat(concurrency): add plan resume"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m4-concurrency-resume` - - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Luis]: Persist step-level progress and implement `plan resume` with graceful shutdown handling. - [ ] Code [Luis]: Add resume checkpoints tied to decision IDs and sandbox checkpoints. - [ ] Code [Luis]: Validate resume eligibility (non-terminal plans only) and emit clear error for invalid states. @@ -6299,6 +6350,7 @@ By Day 30, the system must be able to (all in LOCAL MODE, server connectivity is - [ ] Code [Luis]: Add CLI output for resume summary (phase, step, decision_id) before executing. - [ ] Docs [Luis]: Update plan lifecycle docs for resume behavior. - [ ] Docs [Luis]: Add resume flow example and error cases. + - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Luis]: Add `features/plan_resume.feature` scenarios. - [ ] Tests (Robot) [Luis]: Add resume integration tests. - [ ] Tests (ASV) [Luis]: Add `benchmarks/plan_resume_bench.py` for resume overhead baseline. @@ -6337,16 +6389,16 @@ By Day 30, the system must be able to (all in LOCAL MODE, server connectivity is Deferred items remain planned but are not part of the 30-day MVP scope. -- [ ] **COMMIT (Owner: Hamza | Group: POST.resource-types.virtual-core | Branch: feature/post-resource-types-virtual-core | Planned: Day 31 | Expected: Day 39) - Commit message: "feat(resource): add virtual core resource types"** +- [ ] **COMMIT (Owner: Hamza | Group: POST.resource-types.virtual-core | Branch: feature/post-resource-types-virtual-core | Planned: Day 31 | Expected: Day 44) - Commit message: "feat(resource): add virtual core resource types"** - [ ] Git [Hamza]: `git checkout master` - [ ] Git [Hamza]: `git pull origin master` - [ ] Git [Hamza]: `git checkout -b feature/post-resource-types-virtual-core` - - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Hamza]: Add built-in **virtual** resource type YAML configs under `examples/resource-types/`: `file`, `directory`, `commit`, `branch`, `tag`, `tree`. - [ ] Code [Hamza]: Set `resource_kind: virtual`, `user_addable: false`, and no sandbox strategy; encode allowed children per spec. - [ ] Code [Hamza]: Add equivalence metadata fields (content hash/name/permissions for `file`/`directory`; git object identity for `commit`/`branch`/`tag`/`tree`). - [ ] Code [Hamza]: Extend bootstrap registration to include these virtual types and hide them from `resource add` scaffolding. - [ ] Docs [Hamza]: Update `docs/reference/resource_types_builtin.md` with virtual type descriptions and link semantics. + - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Hamza]: Add scenarios ensuring virtual built-ins exist and cannot be user-added. - [ ] Tests (Robot) [Hamza]: Add Robot test that lists resource types and asserts virtual built-ins are present. - [ ] Tests (ASV) [Hamza]: Add `benchmarks/resource_type_virtual_core_bench.py` for registry performance. @@ -6357,15 +6409,15 @@ Deferred items remain planned but are not part of the 30-day MVP scope. - [ ] Git [Hamza]: `git push -u origin feature/post-resource-types-virtual-core` - [ ] Forgejo PR [Hamza]: Open PR from `feature/post-resource-types-virtual-core` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Hamza | Group: POST.resource-types.physical | Branch: feature/post-resource-types-physical | Planned: Day 32 | Expected: Day 40) - Commit message: "feat(resource): add deferred physical resource types"** +- [ ] **COMMIT (Owner: Hamza | Group: POST.resource-types.physical | Branch: feature/post-resource-types-physical | Planned: Day 32 | Expected: Day 45) - Commit message: "feat(resource): add deferred physical resource types"** - [ ] Git [Hamza]: `git checkout master` - [ ] Git [Hamza]: `git pull origin master` - [ ] Git [Hamza]: `git checkout -b feature/post-resource-types-physical` - - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Hamza]: Add built-in physical resource type YAML configs for git object taxonomy: `git`, `git-remote`, `git-branch`, `git-tag`, `git-commit`, `git-tree`, `git-tree-entry`, `git-stash`, `git-submodule`. - [ ] Code [Hamza]: Add filesystem link types: `fs-symlink`, `fs-hardlink` with correct parent/child constraints. - [ ] Code [Hamza]: Extend auto-discovery rules for git object graph (bounded depth, filtered by type) and fs link detection. - [ ] Docs [Hamza]: Update `docs/reference/resource_types_builtin.md` with deferred physical type flags, parent/child rules, and discovery notes. + - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Hamza]: Add scenarios ensuring deferred physical types register and validate parent/child constraints. - [ ] Tests (Robot) [Hamza]: Add Robot test that lists resource types and asserts the git object taxonomy types. - [ ] Tests (ASV) [Hamza]: Add `benchmarks/resource_type_deferred_physical_bench.py` for registry load overhead. @@ -6376,14 +6428,14 @@ Deferred items remain planned but are not part of the 30-day MVP scope. - [ ] Git [Hamza]: `git push -u origin feature/post-resource-types-physical` - [ ] Forgejo PR [Hamza]: Open PR from `feature/post-resource-types-physical` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Hamza | Group: POST.resource-types.virtual | Branch: feature/post-resource-types-virtual | Planned: Day 33 | Expected: Day 41) - Commit message: "feat(resource): add deferred virtual resource types"** +- [ ] **COMMIT (Owner: Hamza | Group: POST.resource-types.virtual | Branch: feature/post-resource-types-virtual | Planned: Day 33 | Expected: Day 46) - Commit message: "feat(resource): add deferred virtual resource types"** - [ ] Git [Hamza]: `git checkout master` - [ ] Git [Hamza]: `git pull origin master` - [ ] Git [Hamza]: `git checkout -b feature/post-resource-types-virtual` - - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Hamza]: Add built-in virtual resource types: `remote`, `submodule`, `symlink` with equivalence metadata rules. - [ ] Code [Hamza]: Update registry bootstrap to include deferred virtual types and hide them from `resource add` scaffolding. - [ ] Docs [Hamza]: Update `docs/reference/resource_types_builtin.md` with deferred virtual type descriptions and equivalence notes. + - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Hamza]: Add scenarios ensuring deferred virtual types exist and remain non-user-addable. - [ ] Tests (Robot) [Hamza]: Add Robot test that lists resource types and asserts deferred virtual types. - [ ] Tests (ASV) [Hamza]: Add `benchmarks/resource_type_deferred_virtual_bench.py` for registry load overhead. @@ -6394,15 +6446,15 @@ Deferred items remain planned but are not part of the 30-day MVP scope. - [ ] Git [Hamza]: `git push -u origin feature/post-resource-types-virtual` - [ ] Forgejo PR [Hamza]: Open PR from `feature/post-resource-types-virtual` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Luis | Group: POST.safety-profile | Branch: feature/post-safety-profile | Planned: Day 32 | Expected: Day 40) - Commit message: "feat(security): add safety profile model and enforcement stubs"** +- [ ] **COMMIT (Owner: Luis | Group: POST.safety-profile | Branch: feature/post-safety-profile | Planned: Day 32 | Expected: Day 45) - Commit message: "feat(security): add safety profile model and enforcement stubs"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/post-safety-profile` - - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Luis]: Add `SafetyProfile` model with allowed skill categories, sandbox/checkpoint requirements, human-approval flag, and max cost/retry limits per spec. - [ ] Code [Luis]: Add `safety_profile` field to Action model and persistence mapping (no legacy compatibility). - [ ] Code [Luis]: Add plan-level safety resolution stub (plan > action > project > global) that returns NotImplementedError for enforcement in local mode. - [ ] Docs [Luis]: Document safety profile schema, defaults, and stub enforcement behavior in `docs/reference/safety_profile.md`. + - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Luis]: Add model validation scenarios for safety profile field parsing and constraint validation. - [ ] Tests (Robot) [Luis]: Add Robot smoke test that loads a safety profile from YAML and prints serialized output. - [ ] Tests (ASV) [Luis]: Add `benchmarks/safety_profile_model_bench.py` for validation overhead. @@ -6413,15 +6465,15 @@ Deferred items remain planned but are not part of the 30-day MVP scope. - [ ] Git [Luis]: `git push -u origin feature/post-safety-profile` - [ ] Forgejo PR [Luis]: Open PR from `feature/post-safety-profile` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Brent | Group: POST.safety-profile-tests | Branch: feature/post-safety-profile-tests | Planned: Day 33 | Expected: Day 41) - Commit message: "test(security): cover safety profile enforcement"** +- [ ] **COMMIT (Owner: Brent | Group: POST.safety-profile-tests | Branch: feature/post-safety-profile-tests | Planned: Day 33 | Expected: Day 46) - Commit message: "test(security): cover safety profile enforcement"** - [ ] Git [Brent]: `git checkout master` - [ ] Git [Brent]: `git pull origin master` - [ ] Git [Brent]: `git checkout -b feature/post-safety-profile-tests` + - [ ] Docs [Brent]: Add test fixture notes for safety profile YAML examples in `docs/development/testing.md`. - [ ] Git [Brent]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Brent]: Add scenarios for safety profile allow/deny rules and missing profile errors (stub enforcement expected). - [ ] Tests (Behave) [Brent]: Add scenarios for cost/retry bounds validation on action creation. - [ ] Tests (Robot) [Brent]: Add Robot test that verifies safety profile appears in `action show` output. - - [ ] Docs [Brent]: Add test fixture notes for safety profile YAML examples in `docs/development/testing.md`. - [ ] Tests (ASV) [Brent]: Add `benchmarks/safety_profile_tests_bench.py` for scenario runtime baseline. - [ ] Quality [Brent]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. - [ ] Quality [Brent]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it. @@ -6430,11 +6482,10 @@ Deferred items remain planned but are not part of the 30-day MVP scope. - [ ] Git [Brent]: `git push -u origin feature/post-safety-profile-tests` - [ ] Forgejo PR [Brent]: Open PR from `feature/post-safety-profile-tests` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Hamza | Group: POST.resource | Branch: feature/m7-post-resource-equivalence | Planned: Day 34 | Expected: Day 42) - Commit message: "feat(resource): add virtual resource equivalence tracking"** +- [ ] **COMMIT (Owner: Hamza | Group: POST.resource | Branch: feature/m7-post-resource-equivalence | Planned: Day 34 | Expected: Day 47) - Commit message: "feat(resource): add virtual resource equivalence tracking"** - [ ] Git [Hamza]: `git checkout master` - [ ] Git [Hamza]: `git pull origin master` - [ ] Git [Hamza]: `git checkout -b feature/m7-post-resource-equivalence` - - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Hamza]: Add `virtual_resource_links` table mapping virtual resource ULID to physical resource ULIDs with uniqueness constraints. - [ ] Code [Hamza]: Add `ResourceEquivalenceService` to create/merge virtual resources and update links on content divergence. - [ ] Code [Hamza]: Add helper to compute equivalence key (hash or name) for auto-linking during resource discovery. @@ -6444,6 +6495,7 @@ Deferred items remain planned but are not part of the 30-day MVP scope. - [ ] Code [Hamza]: Add guard to prevent linking resources of incompatible types (type mismatch error). - [ ] Docs [Hamza]: Update `docs/reference/resource_model.md` with physical/virtual equivalence rules and examples. - [ ] Docs [Hamza]: Add CLI examples for equivalence list/add/remove. + - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Hamza]: Add scenarios for linking/unlinking physical resources to virtual resources and divergence updates. - [ ] Tests (Robot) [Hamza]: Add Robot test that creates two identical physical resources and verifies a shared virtual resource. - [ ] Tests (ASV) [Hamza]: Add `benchmarks/virtual_resource_bench.py` for equivalence update overhead. @@ -6454,11 +6506,10 @@ Deferred items remain planned but are not part of the 30-day MVP scope. - [ ] Git [Hamza]: `git push -u origin feature/m7-post-resource-equivalence` - [ ] Forgejo PR [Hamza]: Open PR from `feature/m7-post-resource-equivalence` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Luis | Group: POST.server | Branch: feature/m7-post-server | Planned: Day 35 | Expected: Day 43) - Commit message: "feat(client): add server http client"** +- [ ] **COMMIT (Owner: Luis | Group: POST.server | Branch: feature/m7-post-server | Planned: Day 35 | Expected: Day 48) - Commit message: "feat(client): add server http client"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m7-post-server` - - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Luis]: Add HTTP client with health check, version negotiation, and OpenAPI codegen integration. - [ ] Code [Luis]: Add config keys for server base URL, API token, and TLS verification; wire into Settings. - [ ] Code [Luis]: Map server error responses into domain errors with retry hints. @@ -6468,6 +6519,7 @@ Deferred items remain planned but are not part of the 30-day MVP scope. - [ ] Code [Luis]: Add TLS verification toggle and explicit warning when disabled. - [ ] Docs [Luis]: Add `docs/reference/server_client_http.md` with configuration and connection errors. - [ ] Docs [Luis]: Add examples for health check and version negotiation failures. + - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Luis]: Add scenarios for connection errors and version mismatch handling. - [ ] Tests (Robot) [Luis]: Add mock-server connection tests. - [ ] Tests (ASV) [Luis]: Add `benchmarks/server_http_client_bench.py` for connection overhead baseline. @@ -6478,11 +6530,10 @@ Deferred items remain planned but are not part of the 30-day MVP scope. - [ ] Git [Luis]: `git push -u origin feature/m7-post-server` - [ ] Forgejo PR [Luis]: Open PR from `feature/m7-post-server` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Luis | Group: POST.server | Branch: feature/m7-post-server | Planned: Day 36 | Expected: Day 44) - Commit message: "feat(client): add plan sync and remote execution"** +- [ ] **COMMIT (Owner: Luis | Group: POST.server | Branch: feature/m7-post-server | Planned: Day 36 | Expected: Day 49) - Commit message: "feat(client): add plan sync and remote execution"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m7-post-server` - - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Luis]: Sync actions, request remote plan execution/apply/status, and reconcile remote plan IDs. - [ ] Code [Luis]: Add conflict resolution policy (local wins vs server wins) with explicit CLI errors on ambiguity. - [ ] Code [Luis]: Add plan sync scope flags (`--actions/--skills/--tools/--projects`) and default to minimal sync. @@ -6491,6 +6542,7 @@ Deferred items remain planned but are not part of the 30-day MVP scope. - [ ] Code [Luis]: Add dry-run mode to show what would sync without executing changes. - [ ] Docs [Luis]: Document sync semantics and conflict handling in `docs/reference/server_sync.md`. - [ ] Docs [Luis]: Add examples for `agents sync --dry-run` outputs. + - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Luis]: Add scenarios for sync conflicts and retry behavior. - [ ] Tests (Robot) [Luis]: Add mock-server sync tests. - [ ] Tests (ASV) [Luis]: Add `benchmarks/server_sync_bench.py` for sync throughput baseline. @@ -6501,11 +6553,10 @@ Deferred items remain planned but are not part of the 30-day MVP scope. - [ ] Git [Luis]: `git push -u origin feature/m7-post-server` - [ ] Forgejo PR [Luis]: Open PR from `feature/m7-post-server` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Luis | Group: POST.server | Branch: feature/m7-post-server | Planned: Day 37 | Expected: Day 45) - Commit message: "feat(client): add websocket updates"** +- [ ] **COMMIT (Owner: Luis | Group: POST.server | Branch: feature/m7-post-server | Planned: Day 37 | Expected: Day 50) - Commit message: "feat(client): add websocket updates"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m7-post-server` - - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Luis]: Add WebSocket client for plan updates with reconnect/backoff policy. - [ ] Code [Luis]: Define event schema mapping for plan status/progress/log stream updates. - [ ] Code [Luis]: Add heartbeat/ping handling and resume from last event ID on reconnect. @@ -6514,6 +6565,7 @@ Deferred items remain planned but are not part of the 30-day MVP scope. - [ ] Code [Luis]: Add configurable reconnect backoff parameters in settings. - [ ] Docs [Luis]: Add `docs/reference/server_websocket.md` with event types and reconnect rules. - [ ] Docs [Luis]: Add examples of event payloads and resume behavior. + - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Luis]: Add scenarios for reconnect and event ordering. - [ ] Tests (Robot) [Luis]: Add WebSocket mock tests. - [ ] Tests (ASV) [Luis]: Add `benchmarks/server_ws_bench.py` for message handling baseline. @@ -6524,11 +6576,10 @@ Deferred items remain planned but are not part of the 30-day MVP scope. - [ ] Git [Luis]: `git push -u origin feature/m7-post-server` - [ ] Forgejo PR [Luis]: Open PR from `feature/m7-post-server` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Hamza | Group: POST.server | Branch: feature/m7-post-server | Planned: Day 38 | Expected: Day 46) - Commit message: "feat(client): add remote project support"** +- [ ] **COMMIT (Owner: Hamza | Group: POST.server | Branch: feature/m7-post-server | Planned: Day 38 | Expected: Day 51) - Commit message: "feat(client): add remote project support"** - [ ] Git [Hamza]: `git checkout master` - [ ] Git [Hamza]: `git pull origin master` - [ ] Git [Hamza]: `git checkout -b feature/m7-post-server` - - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Hamza]: Add remote resource selection and server execution request wiring. - [ ] Code [Hamza]: Add project-name resolution rules for remote namespaces and server aliases. - [ ] Code [Hamza]: Add `agents project list --remote` and `plan use --remote` scaffolding (server-only). @@ -6537,6 +6588,7 @@ Deferred items remain planned but are not part of the 30-day MVP scope. - [ ] Code [Hamza]: Add CLI output for remote project list (namespace, id, last_updated). - [ ] Docs [Hamza]: Add `docs/reference/server_remote_projects.md` with project selection semantics. - [ ] Docs [Hamza]: Add examples for `project list --remote` and `plan use --remote`. + - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Hamza]: Add scenarios for remote project selection errors. - [ ] Tests (Robot) [Hamza]: Add remote execution mock tests. - [ ] Tests (ASV) [Hamza]: Add `benchmarks/server_remote_project_bench.py` for request overhead baseline. @@ -6570,11 +6622,10 @@ Deferred items remain planned but are not part of the 30-day MVP scope. - [X] Git [Rui]: `git push -u origin feature/m7-post-repl` - [X] Forgejo PR [Rui]: Open PR from `feature/m7-post-repl` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Luis | Group: POST.auth | Branch: feature/m7-post-auth | Planned: Day 37 | Expected: Day 45) - Commit message: "feat(cli): add auth and team commands"** +- [ ] **COMMIT (Owner: Luis | Group: POST.auth | Branch: feature/m7-post-auth | Planned: Day 37 | Expected: Day 50) - Commit message: "feat(cli): add auth and team commands"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m7-post-auth` - - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Luis]: Add `agents auth login/logout/status` and `agents team list/use` commands with stubbed responses when server is disabled. - [ ] Code [Luis]: Add config keys for auth token storage, active team, and default namespace (client-only stubs). - [ ] Code [Luis]: Wire stubbed commands to `AuthClient` and `ServerClient` interfaces (raise NotImplementedError when no server). @@ -6583,6 +6634,7 @@ Deferred items remain planned but are not part of the 30-day MVP scope. - [ ] Code [Luis]: Add token format validation and redaction in all outputs. - [ ] Docs [Luis]: Document auth/team workflows and local-only stub behavior. - [ ] Docs [Luis]: Add examples for login/logout/status and token storage notes. + - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Luis]: Add auth/team CLI scenarios (stubbed responses, missing server errors). - [ ] Tests (Robot) [Luis]: Add auth/team integration smoke tests. - [ ] Tests (ASV) [Luis]: Add `benchmarks/auth_cli_bench.py` for auth command baseline. @@ -6593,16 +6645,16 @@ Deferred items remain planned but are not part of the 30-day MVP scope. - [ ] Git [Luis]: `git push -u origin feature/m7-post-auth` - [ ] Forgejo PR [Luis]: Open PR from `feature/m7-post-auth` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Jeff | Group: POST.tui | Branch: feature/m7-post-tui | Planned: Day 38 | Expected: Day 46) - Commit message: "feat(ui): add TUI/Web interface"** +- [ ] **COMMIT (Owner: Jeff | Group: POST.tui | Branch: feature/m7-post-tui | Planned: Day 38 | Expected: Day 51) - Commit message: "feat(ui): add TUI/Web interface"** - [ ] Git [Jeff]: `git checkout master` - [ ] Git [Jeff]: `git pull origin master` - [ ] Git [Jeff]: `git checkout -b feature/m7-post-tui` - - [ ] Git [Jeff]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Jeff]: Define UI data-provider interface (plans, sessions, validations, diffs, logs) backed by local services. - [ ] Code [Jeff]: Implement minimal TUI with plan list, plan detail, diff viewer, and validation summary panes. - [ ] Code [Jeff]: Add Web UI stub that serves the same data via local-only routes (read-only by default). - [ ] Code [Jeff]: Add auto-refresh interval config and manual refresh keybinds for TUI. - [ ] Docs [Jeff]: Add UI usage guide with navigation and data-refresh behavior. + - [ ] Git [Jeff]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Jeff]: Add UI behavior scenarios (list, detail, diff, refresh). - [ ] Tests (Robot) [Jeff]: Add UI smoke tests for route loading and TUI navigation. - [ ] Tests (ASV) [Jeff]: Add `benchmarks/ui_render_bench.py` for UI render baseline. @@ -6613,15 +6665,15 @@ Deferred items remain planned but are not part of the 30-day MVP scope. - [ ] Git [Jeff]: `git push -u origin feature/m7-post-tui` - [ ] Forgejo PR [Jeff]: Open PR from `feature/m7-post-tui` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Hamza | Group: POST.dbresources | Branch: feature/m7-post-resource-db | Planned: Day 39 | Expected: Day 47) - Commit message: "feat(resource): add database resources"** +- [ ] **COMMIT (Owner: Hamza | Group: POST.dbresources | Branch: feature/m7-post-resource-db | Planned: Day 39 | Expected: Day 52) - Commit message: "feat(resource): add database resources"** - [ ] Git [Hamza]: `git checkout master` - [ ] Git [Hamza]: `git pull origin master` - [ ] Git [Hamza]: `git checkout -b feature/m7-post-resource-db` - - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Hamza]: Add database resource types (postgres, mysql, sqlite, duckdb) with connection args and auth handling. - [ ] Code [Hamza]: Implement sandbox strategy using transaction wrappers and read-only toggles. - [ ] Code [Hamza]: Add connection validation and safe error messaging (mask credentials in logs). - [ ] Docs [Hamza]: Document database resource configuration and supported auth options. + - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Hamza]: Add database resource scenarios (connection validation, read-only enforcement). - [ ] Tests (Robot) [Hamza]: Add database resource integration tests (local sqlite/duckdb only). - [ ] Tests (ASV) [Hamza]: Add `benchmarks/db_resource_bench.py` for resource registration baseline. @@ -6632,15 +6684,15 @@ Deferred items remain planned but are not part of the 30-day MVP scope. - [ ] Git [Hamza]: `git push -u origin feature/m7-post-resource-db` - [ ] Forgejo PR [Hamza]: Open PR from `feature/m7-post-resource-db` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Hamza | Group: POST.cloud | Branch: feature/m7-post-resource-cloud | Planned: Day 40 | Expected: Day 48) - Commit message: "feat(resource): add cloud infrastructure resources"** +- [ ] **COMMIT (Owner: Hamza | Group: POST.cloud | Branch: feature/m7-post-resource-cloud | Planned: Day 40 | Expected: Day 53) - Commit message: "feat(resource): add cloud infrastructure resources"** - [ ] Git [Hamza]: `git checkout master` - [ ] Git [Hamza]: `git pull origin master` - [ ] Git [Hamza]: `git checkout -b feature/m7-post-resource-cloud` - - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Hamza]: Add cloud resource types (aws, gcp, azure) with credential fields and region/tenant metadata. - [ ] Code [Hamza]: Add stubbed sandbox strategies that validate configuration and return NotImplementedError for execution. - [ ] Code [Hamza]: Add credential resolution from environment variables and profile names (no secrets logged). - [ ] Docs [Hamza]: Document cloud resource configuration and local-only stub behavior. + - [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Hamza]: Add cloud resource scenarios (schema validation, stub errors). - [ ] Tests (Robot) [Hamza]: Add cloud resource integration tests with stubbed responses. - [ ] Tests (ASV) [Hamza]: Add `benchmarks/cloud_resource_bench.py` for resource registration baseline. @@ -6651,15 +6703,15 @@ Deferred items remain planned but are not part of the 30-day MVP scope. - [ ] Git [Hamza]: `git push -u origin feature/m7-post-resource-cloud` - [ ] Forgejo PR [Hamza]: Open PR from `feature/m7-post-resource-cloud` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Luis | Group: POST.permissions | Branch: feature/m7-post-permissions | Planned: Day 39 | Expected: Day 47) - Commit message: "feat(security): add permission system"** +- [ ] **COMMIT (Owner: Luis | Group: POST.permissions | Branch: feature/m7-post-permissions | Planned: Day 39 | Expected: Day 52) - Commit message: "feat(security): add permission system"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m7-post-permissions` - - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Luis]: Implement namespace/project/plan/skill permission model (role bindings, default deny, allow overrides). - [ ] Code [Luis]: Add enforcement hooks at CLI/service boundaries (server-only; local mode returns permissive defaults). - [ ] Code [Luis]: Add role enums (owner/admin/editor/viewer) and default role mapping for local mode. - [ ] Docs [Luis]: Document permission model, role matrix, and server-only behavior. + - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Luis]: Add permission scenarios (allow/deny, missing role, server disabled). - [ ] Tests (Robot) [Luis]: Add permission integration tests with stubbed server client. - [ ] Tests (ASV) [Luis]: Add `benchmarks/permission_check_bench.py` for enforcement baseline. @@ -6670,15 +6722,15 @@ Deferred items remain planned but are not part of the 30-day MVP scope. - [ ] Git [Luis]: `git push -u origin feature/m7-post-permissions` - [ ] Forgejo PR [Luis]: Open PR from `feature/m7-post-permissions` to `master` with a suitable and thorough description -- [ ] **COMMIT (Owner: Luis | Group: POST.safety | Branch: feature/m7-post-safety | Planned: Day 40 | Expected: Day 45) - Commit message: "feat(security): add safety profile enforcement"** +- [ ] **COMMIT (Owner: Luis | Group: POST.safety | Branch: feature/m7-post-safety | Planned: Day 40 | Expected: Day 50) - Commit message: "feat(security): add safety profile enforcement"** - [ ] Git [Luis]: `git checkout master` - [ ] Git [Luis]: `git pull origin master` - [ ] Git [Luis]: `git checkout -b feature/m7-post-safety` - - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Code [Luis]: Add SafetyProfile model, CLI flags, and execution enforcement hooks (server-only for now). - [ ] Code [Luis]: Add safety profile resolution order (plan > project > global) with defaults. - [ ] Code [Luis]: Add policy validation for forbidden tools/resources and explicit denial messages. - [ ] Docs [Luis]: Document safety profile options, defaults, and server-only behavior. + - [ ] Git [Luis]: `git fetch origin && git merge origin/master` (run before final tests and before commit) - [ ] Tests (Behave) [Luis]: Add safety profile enforcement scenarios (deny/allow paths). - [ ] Tests (Robot) [Luis]: Add safety profile integration tests with stubbed server client. - [ ] Tests (ASV) [Luis]: Add `benchmarks/safety_profile_bench.py` for enforcement baseline. diff --git a/robot/decision_model.robot b/robot/decision_model.robot new file mode 100644 index 0000000000..b0a54dc022 --- /dev/null +++ b/robot/decision_model.robot @@ -0,0 +1,51 @@ +*** Settings *** +Documentation Smoke tests for decision domain model persistence contract +Resource ${CURDIR}/common.resource +Suite Setup Setup Test Environment +Suite Teardown Cleanup Test Environment + +*** Variables *** +${HELPER_SCRIPT} robot/helper_decision_model.py + +*** Test Cases *** +Create Root Decision + [Documentation] Create a prompt_definition root decision + ${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} create_root cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} decision-create-root-ok + +Create Child Decision + [Documentation] Create a strategy_choice child decision + ${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} create_child cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} decision-create-child-ok + +Decision Type Enum Values + [Documentation] Verify all 11 decision type enum values + ${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} enum_values cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} decision-enums-ok + +Context Snapshot + [Documentation] Create a decision with a populated context snapshot + ${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} context_snapshot cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} decision-snapshot-ok + +Correction Metadata + [Documentation] Create a correction decision and verify metadata + ${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} correction cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} decision-correction-ok + +Round Trip Serialization + [Documentation] Verify model_dump / model_validate round-trip + ${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} roundtrip cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} decision-roundtrip-ok + +CLI Dict Output + [Documentation] Verify as_cli_dict produces expected keys + ${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} cli_dict cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} decision-cli-dict-ok diff --git a/robot/helper_decision_model.py b/robot/helper_decision_model.py new file mode 100644 index 0000000000..6914273489 --- /dev/null +++ b/robot/helper_decision_model.py @@ -0,0 +1,202 @@ +"""Helper script for Robot Framework decision model smoke tests.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +# Ensure src is importable when run from workspace root +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) + +from ulid import ULID + +from cleveragents.domain.models.core.decision import ( + ArtifactRef, + ContextSnapshot, + Decision, + DecisionType, + ResourceRef, +) + + +def _test_create_root() -> None: + """Create a root prompt_definition decision.""" + d = Decision( + plan_id=str(ULID()), + sequence_number=0, + decision_type=DecisionType.PROMPT_DEFINITION, + question="What should we build?", + chosen_option="A REST API for user management", + ) + assert d.is_root + assert d.decision_type == DecisionType.PROMPT_DEFINITION + assert d.parent_decision_id is None + print("decision-create-root-ok") + + +def _test_create_child() -> None: + """Create a strategy_choice child decision.""" + parent_id = str(ULID()) + d = Decision( + plan_id=str(ULID()), + parent_decision_id=parent_id, + sequence_number=1, + decision_type=DecisionType.STRATEGY_CHOICE, + question="Which framework?", + chosen_option="FastAPI", + alternatives_considered=["Flask", "Django"], + confidence_score=0.9, + ) + assert not d.is_root + assert d.parent_decision_id == parent_id + assert d.confidence_score == 0.9 + assert len(d.alternatives_considered) == 2 + print("decision-create-child-ok") + + +def _test_enum_values() -> None: + """Verify all 11 decision type enum values.""" + expected = { + "prompt_definition", + "invariant_enforced", + "strategy_choice", + "implementation_choice", + "resource_selection", + "subplan_spawn", + "subplan_parallel_spawn", + "tool_invocation", + "error_recovery", + "validation_response", + "user_intervention", + } + actual = {dt.value for dt in DecisionType} + assert actual == expected, f"Mismatch: {actual.symmetric_difference(expected)}" + assert len(DecisionType) == 11 + print("decision-enums-ok") + + +def _test_context_snapshot() -> None: + """Create a decision with a populated context snapshot.""" + snap = ContextSnapshot( + hot_context_hash="sha256:abc123def", + hot_context_ref="store://snapshots/abc123def", + relevant_resources=[ + ResourceRef(resource_id=str(ULID()), path="src/main.py"), + ResourceRef(resource_id=str(ULID())), + ], + actor_state_ref="checkpoint://actor/001", + ) + d = Decision( + plan_id=str(ULID()), + sequence_number=0, + decision_type=DecisionType.PROMPT_DEFINITION, + question="What approach?", + chosen_option="Microservices", + context_snapshot=snap, + ) + assert d.context_snapshot.hot_context_hash == "sha256:abc123def" + assert len(d.context_snapshot.relevant_resources) == 2 + assert d.context_snapshot.actor_state_ref == "checkpoint://actor/001" + print("decision-snapshot-ok") + + +def _test_correction() -> None: + """Create a correction decision and verify metadata.""" + original_id = str(ULID()) + d = Decision( + plan_id=str(ULID()), + parent_decision_id=str(ULID()), + sequence_number=3, + decision_type=DecisionType.STRATEGY_CHOICE, + question="Which framework?", + chosen_option="Django instead", + is_correction=True, + corrects_decision_id=original_id, + correction_reason="FastAPI lacks admin panel", + ) + assert d.is_correction + assert d.corrects_decision_id == original_id + assert d.correction_reason == "FastAPI lacks admin panel" + assert not d.is_superseded + print("decision-correction-ok") + + +def _test_roundtrip() -> None: + """Verify model_dump / model_validate round-trip.""" + d = Decision( + plan_id=str(ULID()), + parent_decision_id=str(ULID()), + sequence_number=5, + decision_type=DecisionType.IMPLEMENTATION_CHOICE, + question="How to implement auth?", + chosen_option="JWT tokens", + confidence_score=0.85, + context_snapshot=ContextSnapshot( + hot_context_hash="sha256:roundtrip", + relevant_resources=[ResourceRef(resource_id=str(ULID()))], + ), + artifacts_produced=[ + ArtifactRef(artifact_path="src/auth.py", artifact_type="file"), + ], + ) + data = d.model_dump() + restored = Decision.model_validate(data) + assert restored.decision_id == d.decision_id + assert restored.decision_type == d.decision_type + assert restored.confidence_score == d.confidence_score + assert restored.context_snapshot.hot_context_hash == "sha256:roundtrip" + assert len(restored.artifacts_produced) == 1 + print("decision-roundtrip-ok") + + +def _test_cli_dict() -> None: + """Verify as_cli_dict produces expected keys.""" + d = Decision( + plan_id=str(ULID()), + sequence_number=0, + decision_type=DecisionType.PROMPT_DEFINITION, + question="Build what?", + chosen_option="API", + ) + cli = d.as_cli_dict() + required_keys = { + "decision_id", + "plan_id", + "type", + "sequence", + "question", + "chosen", + "confidence", + "parent", + "is_correction", + "superseded", + } + assert required_keys.issubset(cli.keys()), ( + f"Missing keys: {required_keys - cli.keys()}" + ) + print("decision-cli-dict-ok") + + +_TESTS = { + "create_root": _test_create_root, + "create_child": _test_create_child, + "enum_values": _test_enum_values, + "context_snapshot": _test_context_snapshot, + "correction": _test_correction, + "roundtrip": _test_roundtrip, + "cli_dict": _test_cli_dict, +} + +if __name__ == "__main__": + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} ") + print(f"Available tests: {', '.join(sorted(_TESTS))}") + sys.exit(1) + + test_name = sys.argv[1] + if test_name not in _TESTS: + print(f"Unknown test: {test_name}") + print(f"Available: {', '.join(sorted(_TESTS))}") + sys.exit(1) + + _TESTS[test_name]() diff --git a/robot/helper_resource_handlers.py b/robot/helper_resource_handlers.py new file mode 100644 index 0000000000..a38f38ecba --- /dev/null +++ b/robot/helper_resource_handlers.py @@ -0,0 +1,147 @@ +"""Helper utilities for resource handler Robot smoke tests. + +Each command prints a single ``-ok`` token on success so the +calling Robot test can assert on ``stdout``. +""" + +from __future__ import annotations + +import sys +from datetime import datetime +from unittest.mock import MagicMock, PropertyMock + +from cleveragents.domain.models.core.resource import ( + PhysVirt, + Resource, + ResourceCapabilities, +) +from cleveragents.infrastructure.sandbox.factory import SandboxFactory +from cleveragents.infrastructure.sandbox.manager import SandboxManager +from cleveragents.infrastructure.sandbox.protocol import ( + SandboxContext, + SandboxStatus, +) +from cleveragents.resource.handlers.fs_directory import FsDirectoryHandler +from cleveragents.resource.handlers.git_checkout import GitCheckoutHandler +from cleveragents.resource.handlers.protocol import ResourceHandler +from cleveragents.resource.handlers.resolver import ( + HandlerResolutionError, + clear_handler_cache, + resolve_handler, +) +from cleveragents.tool.context import BoundResource + + +def _make_resource(rtype: str, location: str) -> Resource: + """Create a test resource with a valid ULID.""" + return Resource( + resource_id="01KJ5C5TPMP8GGX3QC83E2MAQS", + resource_type_name=rtype, + classification=PhysVirt.PHYSICAL, + location=location, + capabilities=ResourceCapabilities( + readable=True, writable=True, sandboxable=True + ), + ) + + +def _make_mock_manager() -> SandboxManager: + """Create a SandboxManager with a mock factory.""" + mock_factory = MagicMock(spec=SandboxFactory) + mock_sandbox = MagicMock() + mock_sandbox.sandbox_id = "sb-robot-001" + type(mock_sandbox).status = PropertyMock(return_value=SandboxStatus.CREATED) + mock_sandbox.context = SandboxContext( + sandbox_id="sb-robot-001", + sandbox_path="/tmp/sandbox/sb-robot-001", + original_path="/tmp/original", + resource_id="res-robot", + plan_id="plan-robot", + created_at=datetime.now(), + ) + mock_sandbox.create.return_value = mock_sandbox.context + mock_factory.create_sandbox.return_value = mock_sandbox + return SandboxManager(factory=mock_factory, cleanup_on_exit=False) + + +def _protocol_check() -> None: + """Verify both handlers satisfy the ResourceHandler protocol.""" + git = GitCheckoutHandler() + fs = FsDirectoryHandler() + assert isinstance(git, ResourceHandler), "GitCheckoutHandler not a ResourceHandler" + assert isinstance(fs, ResourceHandler), "FsDirectoryHandler not a ResourceHandler" + print("protocol-check-ok") + + +def _git_resolve() -> None: + """Verify GitCheckoutHandler resolves a resource.""" + resource = _make_resource("git-checkout", "/tmp/test-repo") + manager = _make_mock_manager() + handler = GitCheckoutHandler() + bound = handler.resolve( + resource=resource, + plan_id="PLAN_ROBOT", + slot_name="repo", + sandbox_manager=manager, + ) + assert isinstance(bound, BoundResource) + assert bound.sandbox_path, "sandbox_path empty" + assert bound.resource_type == "git-checkout" + print("git-resolve-ok") + + +def _fs_resolve() -> None: + """Verify FsDirectoryHandler resolves a resource.""" + resource = _make_resource("fs-directory", "/tmp/test-dir") + manager = _make_mock_manager() + handler = FsDirectoryHandler() + bound = handler.resolve( + resource=resource, + plan_id="PLAN_ROBOT", + slot_name="workdir", + sandbox_manager=manager, + ) + assert isinstance(bound, BoundResource) + assert bound.sandbox_path, "sandbox_path empty" + assert bound.resource_type == "fs-directory" + print("fs-resolve-ok") + + +def _resolver_import() -> None: + """Verify handler resolver loads handlers from module:class strings.""" + clear_handler_cache() + git = resolve_handler( + "cleveragents.resource.handlers.git_checkout:GitCheckoutHandler" + ) + assert isinstance(git, GitCheckoutHandler) + fs = resolve_handler( + "cleveragents.resource.handlers.fs_directory:FsDirectoryHandler" + ) + assert isinstance(fs, FsDirectoryHandler) + print("resolver-import-ok") + + +def _resolver_error() -> None: + """Verify handler resolver raises on bad references.""" + clear_handler_cache() + try: + resolve_handler("nonexistent.module:FakeClass") + print("resolver-error-FAIL") + except HandlerResolutionError: + print("resolver-error-ok") + + +_COMMANDS = { + "protocol-check": _protocol_check, + "git-resolve": _git_resolve, + "fs-resolve": _fs_resolve, + "resolver-import": _resolver_import, + "resolver-error": _resolver_error, +} + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS: + print(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>", file=sys.stderr) + sys.exit(1) + _COMMANDS[sys.argv[1]]() diff --git a/robot/resource_handlers.robot b/robot/resource_handlers.robot new file mode 100644 index 0000000000..ad4730bde7 --- /dev/null +++ b/robot/resource_handlers.robot @@ -0,0 +1,49 @@ +*** Settings *** +Documentation Smoke tests for resource handler runtime +Resource ${CURDIR}/common.resource +Suite Setup Setup Test Environment +Suite Teardown Cleanup Test Environment + +*** Variables *** +${HELPER} ${CURDIR}/helper_resource_handlers.py + +*** Test Cases *** +Handler Protocol Conformance + [Documentation] Verify GitCheckoutHandler and FsDirectoryHandler satisfy ResourceHandler + ${result}= Run Process ${PYTHON} ${HELPER} protocol-check cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} protocol-check-ok + +GitCheckout Handler Resolution + [Documentation] Verify GitCheckoutHandler resolves a git-checkout resource to a sandbox + ${result}= Run Process ${PYTHON} ${HELPER} git-resolve cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} git-resolve-ok + +FsDirectory Handler Resolution + [Documentation] Verify FsDirectoryHandler resolves an fs-directory resource to a sandbox + ${result}= Run Process ${PYTHON} ${HELPER} fs-resolve cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} fs-resolve-ok + +Handler Resolver Dynamic Import + [Documentation] Verify resolve_handler loads handlers from module:class strings + ${result}= Run Process ${PYTHON} ${HELPER} resolver-import cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} resolver-import-ok + +Handler Resolver Error On Bad Reference + [Documentation] Verify resolve_handler raises HandlerResolutionError on bad references + ${result}= Run Process ${PYTHON} ${HELPER} resolver-error cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} resolver-error-ok diff --git a/src/cleveragents/application/services/resource_handler_service.py b/src/cleveragents/application/services/resource_handler_service.py new file mode 100644 index 0000000000..e09a816ce3 --- /dev/null +++ b/src/cleveragents/application/services/resource_handler_service.py @@ -0,0 +1,298 @@ +"""Resource handler orchestration service. + +The :class:`ResourceHandlerService` is the **orchestration bridge** that +connects the existing infrastructure pieces into a complete resource +resolution pipeline: + +1. :class:`BindingResolutionService` resolves tool slots to + :class:`BindingResult` (resource_id). +2. :class:`ResourceRegistryService` looks up the :class:`Resource` to + get ``location`` and ``resource_type_name``. +3. :class:`ResourceRegistryService` looks up the :class:`ResourceTypeSpec` + to get ``sandbox_strategy`` and ``handler`` reference. +4. :func:`resolve_handler` dynamically loads the handler class. +5. The handler calls :meth:`SandboxManager.get_or_create_sandbox` and + returns a :class:`BoundResource` with ``sandbox_path`` populated. + +The service also provides a convenience method that resolves ALL tool +bindings in one call, producing a ``dict[str, BoundResource]`` ready +for :class:`PlanExecutionContext`. + +Based on: + - implementation_plan.md group M1.resource-handlers (L2254-L2271) +""" + +from __future__ import annotations + +import logging +from collections.abc import Callable +from typing import cast + +from cleveragents.core.exceptions import NotFoundError +from cleveragents.domain.models.core.resource import Resource +from cleveragents.domain.models.core.resource_slot import BindingResult +from cleveragents.domain.models.core.resource_type import ResourceTypeSpec +from cleveragents.infrastructure.sandbox.factory import SandboxStrategyStr +from cleveragents.infrastructure.sandbox.manager import SandboxManager +from cleveragents.resource.handlers.protocol import ResourceHandler +from cleveragents.resource.handlers.resolver import ( + HandlerResolutionError, + resolve_handler, +) +from cleveragents.tool.context import BoundResource + +logger = logging.getLogger(__name__) + + +class ResourceHandlerService: + """Orchestrates resource resolution from binding results to bound resources. + + Bridges the gap between :class:`BindingResolutionService` output + (``BindingResult`` with ``resource_id``) and the execution context's + ``BoundResource`` (with ``sandbox_path``). + + Parameters + ---------- + sandbox_manager: + The sandbox lifecycle manager for creating/reusing sandboxes. + resource_lookup: + Callable that takes a resource name-or-id and returns a + :class:`Resource`. Typically + ``ResourceRegistryService.show_resource``. + type_lookup: + Callable that takes a type name and returns a + :class:`ResourceTypeSpec`. Typically + ``ResourceRegistryService.show_type``. + """ + + def __init__( + self, + *, + sandbox_manager: SandboxManager, + resource_lookup: Callable[[str], Resource], + type_lookup: Callable[[str], ResourceTypeSpec], + ) -> None: + self._sandbox_manager = sandbox_manager + self._resource_lookup = resource_lookup + self._type_lookup = type_lookup + self._logger = logger + + def resolve_binding( + self, + binding: BindingResult, + plan_id: str, + access: str = "read_only", + ) -> BoundResource: + """Resolve a single binding result into a BoundResource. + + Steps: + 1. Look up the Resource by ``binding.resource_id``. + 2. Look up the ResourceTypeSpec by ``resource.resource_type_name``. + 3. Resolve or fallback the handler. + 4. Call ``handler.resolve()`` to provision a sandbox and + produce a BoundResource. + + Args: + binding: A resolved binding with a ``resource_id``. + plan_id: The plan requesting the resolution. + access: Access mode for the bound resource. + + Returns: + A :class:`BoundResource` with ``sandbox_path`` populated. + + Raises: + ValueError: If the binding has no resource_id (deferred). + NotFoundError: If the resource or type is not found. + HandlerResolutionError: If the handler cannot be loaded. + """ + if binding.deferred or not binding.resource_id: + raise ValueError( + f"Cannot resolve deferred binding for slot '{binding.slot_name}'" + ) + + # Step 1: Look up resource + resource: Resource = self._resource_lookup(binding.resource_id) + + # Step 2: Look up type spec + type_spec: ResourceTypeSpec = self._type_lookup(resource.resource_type_name) + + # Step 3: Resolve handler + handler = self._resolve_handler_for_type(type_spec, resource) + + # Step 4: Delegate to handler + return handler.resolve( + resource=resource, + plan_id=plan_id, + slot_name=binding.slot_name, + sandbox_manager=self._sandbox_manager, + access=access, + ) + + def resolve_bindings( + self, + bindings: list[BindingResult], + plan_id: str, + access: str = "read_only", + ) -> dict[str, BoundResource]: + """Resolve all non-deferred bindings into BoundResources. + + Deferred bindings (parameter mode) are silently skipped. + + Args: + bindings: List of binding results from + :class:`BindingResolutionService`. + plan_id: The plan requesting the resolution. + access: Default access mode for all bindings. + + Returns: + A dict mapping slot names to :class:`BoundResource` objects. + """ + result: dict[str, BoundResource] = {} + for binding in bindings: + if binding.deferred or not binding.resource_id: + self._logger.debug( + "Skipping deferred binding for slot '%s'", + binding.slot_name, + ) + continue + + try: + bound = self.resolve_binding( + binding=binding, + plan_id=plan_id, + access=access, + ) + result[binding.slot_name] = bound + self._logger.info( + "Resolved binding slot='%s' -> resource='%s' sandbox='%s'", + binding.slot_name, + binding.resource_id, + bound.sandbox_path, + ) + except (NotFoundError, HandlerResolutionError, ValueError) as exc: + self._logger.warning( + "Failed to resolve binding for slot '%s': %s", + binding.slot_name, + exc, + ) + raise + + return result + + def resolve_resource( + self, + resource: Resource, + plan_id: str, + slot_name: str, + access: str = "read_only", + ) -> BoundResource: + """Resolve a resource directly (without going through BindingResult). + + Convenience method when you already have the Resource object. + + Args: + resource: The resource to resolve. + plan_id: The plan requesting the resolution. + slot_name: Name for the binding slot. + access: Access mode. + + Returns: + A :class:`BoundResource` with ``sandbox_path`` populated. + """ + type_spec = self._type_lookup(resource.resource_type_name) + handler = self._resolve_handler_for_type(type_spec, resource) + return handler.resolve( + resource=resource, + plan_id=plan_id, + slot_name=slot_name, + sandbox_manager=self._sandbox_manager, + access=access, + ) + + def _resolve_handler_for_type( + self, + type_spec: ResourceTypeSpec, + resource: Resource, + ) -> ResourceHandler: + """Resolve the handler for a resource type spec. + + If the type spec has a handler reference string, resolves it + dynamically. Otherwise falls back to a default handler based + on the sandbox strategy. + + Args: + type_spec: The resource type specification. + resource: The resource being resolved. + + Returns: + A handler instance satisfying :class:`ResourceHandler`. + """ + if type_spec.handler: + try: + return resolve_handler(type_spec.handler) + except HandlerResolutionError as exc: + self._logger.debug( + "Handler resolution error for '%s': %s", + type_spec.handler, + exc, + ) + self._logger.warning( + "Failed to resolve handler '%s' for type '%s', " + "falling back to default handler", + type_spec.handler, + type_spec.name, + ) + + # Fallback: use DefaultHandler which delegates to SandboxManager + return _DefaultHandler(type_spec=type_spec) + + +class _DefaultHandler: + """Fallback handler when no handler class is specified or resolution fails. + + Delegates directly to :class:`SandboxManager` using the type's + default sandbox strategy (or resource override). + """ + + def __init__(self, type_spec: ResourceTypeSpec) -> None: + self._type_spec = type_spec + + def resolve( + self, + *, + resource: Resource, + plan_id: str, + slot_name: str, + sandbox_manager: SandboxManager, + access: str = "read_only", + ) -> BoundResource: + """Resolve using the default sandbox strategy.""" + if not resource.location: + raise ValueError(f"Resource '{resource.resource_id}' has no location") + + # resource.sandbox_strategy may be a StrEnum or plain str (Pydantic coercion) + if resource.sandbox_strategy: + strategy_str = str(resource.sandbox_strategy) + else: + strategy_str = str(self._type_spec.sandbox_strategy) + + sandbox = sandbox_manager.get_or_create_sandbox( + plan_id=plan_id, + resource_id=resource.resource_id, + original_path=resource.location, + sandbox_strategy=cast(SandboxStrategyStr, strategy_str), + ) + + if sandbox.context is None: + raise RuntimeError( + f"Sandbox for resource '{resource.resource_id}' " + f"(plan={plan_id}) was created but has no context" + ) + + return BoundResource( + slot_name=slot_name, + resource_id=resource.resource_id, + resource_type=resource.resource_type_name, + sandbox_path=sandbox.context.sandbox_path, + access=access, + ) diff --git a/src/cleveragents/domain/models/core/decision.py b/src/cleveragents/domain/models/core/decision.py new file mode 100644 index 0000000000..41c3edf6bd --- /dev/null +++ b/src/cleveragents/domain/models/core/decision.py @@ -0,0 +1,490 @@ +"""Decision domain models for the decision tree subsystem. + +A :class:`Decision` represents a persisted choice point in a plan's +decision tree, created during the Strategize or Execute phases. Each +decision records the question being answered, the chosen option, +alternatives considered, a confidence score, rationale, a +:class:`ContextSnapshot` for replay, and downstream dependency links. + +Decision types +-------------- + +.. list-table:: + :header-rows: 1 + + * - Type + - Phase + - Description + * - ``prompt_definition`` + - Strategize + - Root decision — the plan prompt + * - ``invariant_enforced`` + - Strategize + - An invariant constraint was applied + * - ``strategy_choice`` + - Strategize + - High-level approach chosen + * - ``implementation_choice`` + - Execute + - How to implement a specific task + * - ``resource_selection`` + - Execute + - Which resources to read / modify + * - ``subplan_spawn`` + - Strategize + - Decision to create a child plan + * - ``subplan_parallel_spawn`` + - Strategize + - Spawn a group of child plans in parallel + * - ``tool_invocation`` + - Execute + - Which skill / tool to use + * - ``error_recovery`` + - Execute + - How to handle a failure + * - ``validation_response`` + - Execute + - Response to a validation failure + * - ``user_intervention`` + - Any + - User-provided guidance / correction + +Context snapshots +----------------- + +Every decision captures a :class:`ContextSnapshot` sufficient to +replay the decision from the same informational starting point. The +snapshot includes a hash of the hot context window, a storage +reference for the full context, a list of :class:`ResourceRef` +entries, and the LangGraph actor checkpoint reference. + +Based on: + - docs/specification.md L18390-L18521 + - docs/adr/ADR-007-decision-tree-and-correction.md + - docs/adr/ADR-033-decision-recording-protocol.md + - docs/adr/ADR-034-decision-tree-versioning-and-history.md +""" + +from __future__ import annotations + +import re +from datetime import UTC, datetime +from enum import StrEnum + +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +from ulid import ULID + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +ULID_PATTERN = r"^[0-9A-HJKMNP-TV-Z]{26}$" +_ULID_RE = re.compile(ULID_PATTERN) + + +# --------------------------------------------------------------------------- +# Enums +# --------------------------------------------------------------------------- + + +class DecisionType(StrEnum): + """Classification of a decision node in the plan decision tree. + + Each type is associated with one or more plan phases and constrains + what downstream decisions are permitted. + """ + + PROMPT_DEFINITION = "prompt_definition" + INVARIANT_ENFORCED = "invariant_enforced" + STRATEGY_CHOICE = "strategy_choice" + IMPLEMENTATION_CHOICE = "implementation_choice" + RESOURCE_SELECTION = "resource_selection" + SUBPLAN_SPAWN = "subplan_spawn" + SUBPLAN_PARALLEL_SPAWN = "subplan_parallel_spawn" + TOOL_INVOCATION = "tool_invocation" + ERROR_RECOVERY = "error_recovery" + VALIDATION_RESPONSE = "validation_response" + USER_INTERVENTION = "user_intervention" + + +#: Decision types that may only be created during the Strategize phase. +STRATEGIZE_TYPES: frozenset[DecisionType] = frozenset( + { + DecisionType.PROMPT_DEFINITION, + DecisionType.INVARIANT_ENFORCED, + DecisionType.STRATEGY_CHOICE, + DecisionType.SUBPLAN_SPAWN, + DecisionType.SUBPLAN_PARALLEL_SPAWN, + } +) + +#: Decision types that may only be created during the Execute phase. +EXECUTE_TYPES: frozenset[DecisionType] = frozenset( + { + DecisionType.IMPLEMENTATION_CHOICE, + DecisionType.RESOURCE_SELECTION, + DecisionType.TOOL_INVOCATION, + DecisionType.ERROR_RECOVERY, + DecisionType.VALIDATION_RESPONSE, + } +) + + +# --------------------------------------------------------------------------- +# Embedded value objects +# --------------------------------------------------------------------------- + + +class ResourceRef(BaseModel): + """Reference to a resource that influenced a decision. + + Captures the resource identifier and an optional path / symbol + within that resource for fine-grained provenance tracking. + """ + + model_config = ConfigDict(frozen=True, str_strip_whitespace=True) + + resource_id: str = Field( + ..., + description="ULID of the referenced resource.", + ) + path: str = Field( + default="", + description="Optional sub-path or symbol within the resource.", + ) + + @field_validator("resource_id") + @classmethod + def _resource_id_not_empty(cls, v: str) -> str: + if not v or not v.strip(): + raise ValueError("resource_id must not be empty") + return v + + +class ArtifactRef(BaseModel): + """Reference to an artifact produced by a decision. + + Artifacts are files, patches, or outputs created as a side-effect + of executing a decision. + """ + + model_config = ConfigDict(frozen=True, str_strip_whitespace=True) + + artifact_path: str = Field( + ..., + description="Path to the artifact (relative to workspace root).", + ) + artifact_type: str = Field( + default="file", + description="Kind of artifact: file, patch, log, etc.", + ) + + @field_validator("artifact_path") + @classmethod + def _artifact_path_not_empty(cls, v: str) -> str: + if not v or not v.strip(): + raise ValueError("artifact_path must not be empty") + return v + + +class ContextSnapshot(BaseModel): + """Snapshot of the informational context at decision time. + + Captures enough state to replay the decision from the same + starting point. The ``hot_context_hash`` is a cryptographic hash + of the exact context window; ``hot_context_ref`` is a storage + pointer to the full serialised context. + + See ADR-033 §Context Snapshot Auto-Capture for the recording + protocol. + """ + + model_config = ConfigDict(frozen=True, str_strip_whitespace=True) + + hot_context_hash: str = Field( + default="", + description="Cryptographic hash of the hot context window.", + ) + hot_context_ref: str = Field( + default="", + description="Storage pointer to the full context snapshot.", + ) + relevant_resources: list[ResourceRef] = Field( + default_factory=list, + description="Resources that influenced this decision.", + ) + actor_state_ref: str = Field( + default="", + description="LangGraph actor checkpoint reference.", + ) + + +# --------------------------------------------------------------------------- +# Decision model +# --------------------------------------------------------------------------- + + +class Decision(BaseModel): + """A persisted choice point in a plan's decision tree. + + Decisions form a tree (via ``parent_decision_id``) and an influence + DAG (via ``downstream_decision_ids``). The ``prompt_definition`` + type is always the root of the tree and must have no parent. + + Immutable once created — corrections create *new* decisions and + set ``superseded_by`` on the original. + + .. note:: + + Because the model is frozen, ``superseded_by`` cannot be set + via attribute assignment. Use :meth:`with_superseded_by` to + obtain a copy with the field populated. The persistence layer + is responsible for writing the updated copy back to storage. + """ + + model_config = ConfigDict( + frozen=True, + str_strip_whitespace=True, + use_enum_values=False, + ) + + # --- Identity --- + + decision_id: str = Field( + default_factory=lambda: str(ULID()), + description="Unique decision identifier (ULID).", + pattern=ULID_PATTERN, + ) + plan_id: str = Field( + ..., + description="ULID of the parent plan.", + pattern=ULID_PATTERN, + ) + parent_decision_id: str | None = Field( + default=None, + description="ULID of the parent decision (None for root).", + ) + sequence_number: int = Field( + ..., + ge=0, + description=( + "Monotonic order within the plan's decisions. " + "Uniqueness within a plan is enforced at the persistence " + "layer, not the domain model." + ), + ) + + # --- Classification --- + + decision_type: DecisionType = Field( + ..., + description="The kind of decision being recorded.", + ) + + # --- The decision itself --- + + question: str = Field( + ..., + min_length=1, + description="What question was being answered.", + ) + chosen_option: str = Field( + ..., + min_length=1, + description="The option that was chosen.", + ) + alternatives_considered: list[str] = Field( + default_factory=list, + description="Other options that were evaluated.", + ) + confidence_score: float | None = Field( + default=None, + ge=0.0, + le=1.0, + description="Confidence in the decision (0.0-1.0).", + ) + + # --- Context snapshot --- + + context_snapshot: ContextSnapshot = Field( + default_factory=ContextSnapshot, + description="Informational context at decision time.", + ) + + # --- Rationale --- + + rationale: str = Field( + default="", + description="Human-readable rationale for the decision.", + ) + actor_reasoning: str | None = Field( + default=None, + description="Raw LLM reasoning trace, if available.", + ) + + # --- Downstream impact --- + + downstream_decision_ids: list[str] = Field( + default_factory=list, + description="ULIDs of decisions that depend on this one.", + ) + downstream_plan_ids: list[str] = Field( + default_factory=list, + description="ULIDs of child plans spawned from this decision.", + ) + artifacts_produced: list[ArtifactRef] = Field( + default_factory=list, + description="Artifacts created as side-effects.", + ) + + # --- Timestamps --- + + created_at: datetime = Field( + default_factory=lambda: datetime.now(UTC), + description="When the decision was recorded.", + ) + + # --- Correction metadata --- + + is_correction: bool = Field( + default=False, + description="Whether this decision is a correction of another.", + ) + corrects_decision_id: str | None = Field( + default=None, + description="ULID of the decision being corrected.", + ) + correction_reason: str | None = Field( + default=None, + description="Why the correction was made.", + ) + superseded_by: str | None = Field( + default=None, + description="ULID of the decision that supersedes this one.", + ) + + # --- Validators --- + + @field_validator("plan_id") + @classmethod + def _plan_id_valid(cls, v: str) -> str: + if not _ULID_RE.match(v): + raise ValueError(f"plan_id must be a valid ULID, got '{v}'") + return v + + @field_validator("parent_decision_id") + @classmethod + def _parent_decision_id_valid(cls, v: str | None) -> str | None: + if v is not None and not _ULID_RE.match(v): + raise ValueError( + f"parent_decision_id must be a valid ULID or None, got '{v}'" + ) + return v + + @field_validator("corrects_decision_id") + @classmethod + def _corrects_decision_id_valid(cls, v: str | None) -> str | None: + if v is not None and not _ULID_RE.match(v): + raise ValueError( + f"corrects_decision_id must be a valid ULID or None, got '{v}'" + ) + return v + + @field_validator("superseded_by") + @classmethod + def _superseded_by_valid(cls, v: str | None) -> str | None: + if v is not None and not _ULID_RE.match(v): + raise ValueError(f"superseded_by must be a valid ULID or None, got '{v}'") + return v + + @model_validator(mode="after") + def _root_decision_constraints(self) -> Decision: + """Ensure prompt_definition decisions have no parent.""" + if ( + self.decision_type == DecisionType.PROMPT_DEFINITION + and self.parent_decision_id is not None + ): + raise ValueError( + "prompt_definition decisions must be the tree root " + "(parent_decision_id must be None)" + ) + return self + + @model_validator(mode="after") + def _correction_fields_consistent(self) -> Decision: + """Ensure correction metadata is internally consistent.""" + if self.is_correction and not self.corrects_decision_id: + raise ValueError( + "is_correction is True but corrects_decision_id is not set" + ) + if self.corrects_decision_id and not self.is_correction: + raise ValueError("corrects_decision_id is set but is_correction is False") + return self + + # --- Domain helpers --- + + @property + def is_root(self) -> bool: + """Return True if this is the root of the decision tree.""" + return self.parent_decision_id is None + + @property + def is_superseded(self) -> bool: + """Return True if this decision has been superseded by a correction.""" + return self.superseded_by is not None + + @property + def is_strategize_type(self) -> bool: + """Return True if this is a Strategize-phase decision type.""" + return self.decision_type in STRATEGIZE_TYPES + + @property + def is_execute_type(self) -> bool: + """Return True if this is an Execute-phase decision type.""" + return self.decision_type in EXECUTE_TYPES + + @property + def is_any_phase_type(self) -> bool: + """Return True if this decision type is valid in any phase.""" + return self.decision_type == DecisionType.USER_INTERVENTION + + def as_cli_dict(self) -> dict[str, object]: + """Return a stable dict for CLI table rendering.""" + return { + "decision_id": self.decision_id, + "plan_id": self.plan_id, + "type": str(self.decision_type), + "sequence": self.sequence_number, + "question": self.question, + "chosen": self.chosen_option, + "confidence": self.confidence_score, + "parent": self.parent_decision_id or "(root)", + "is_correction": self.is_correction, + "superseded": self.is_superseded, + } + + def with_superseded_by(self, new_decision_id: str) -> Decision: + """Return a copy with ``superseded_by`` set. + + Because the model is frozen, this is the canonical way to mark + a decision as superseded during the correction lifecycle. + + Args: + new_decision_id: ULID of the decision that supersedes this one. + + Returns: + A new :class:`Decision` instance identical to this one + except with ``superseded_by`` populated. + """ + return self.model_copy(update={"superseded_by": new_decision_id}) + + +__all__ = [ + "EXECUTE_TYPES", + "STRATEGIZE_TYPES", + "ArtifactRef", + "ContextSnapshot", + "Decision", + "DecisionType", + "ResourceRef", +] diff --git a/src/cleveragents/resource/handlers/__init__.py b/src/cleveragents/resource/handlers/__init__.py new file mode 100644 index 0000000000..57ae71361b --- /dev/null +++ b/src/cleveragents/resource/handlers/__init__.py @@ -0,0 +1,43 @@ +"""Resource handler implementations for CleverAgents. + +This package provides the handler runtime that bridges resource types +to sandbox provisioning. Each handler resolves a :class:`Resource` +into a sandboxed working path via the :class:`SandboxManager`. + +## Handler Protocol + +All handlers implement +:class:`~cleveragents.resource.handlers.protocol.ResourceHandler`, +which defines a single ``resolve`` method returning a +:class:`~cleveragents.tool.context.BoundResource` with a populated +``sandbox_path``. + +## Built-in Handlers + +| Handler | Resource Type | Sandbox Strategy | +|-----------------------|------------------|------------------| +| ``GitCheckoutHandler``| ``git-checkout`` | ``git_worktree`` | +| ``FsDirectoryHandler``| ``fs-directory`` | ``copy_on_write``| + +## Handler Resolution + +Handler strings stored on :class:`ResourceTypeSpec` use the format +``module.path:ClassName``. The :func:`resolve_handler` function +dynamically imports the module and returns an instance. +""" + +from cleveragents.resource.handlers.fs_directory import FsDirectoryHandler +from cleveragents.resource.handlers.git_checkout import GitCheckoutHandler +from cleveragents.resource.handlers.protocol import ResourceHandler +from cleveragents.resource.handlers.resolver import ( + HandlerResolutionError, + resolve_handler, +) + +__all__ = [ + "FsDirectoryHandler", + "GitCheckoutHandler", + "HandlerResolutionError", + "ResourceHandler", + "resolve_handler", +] diff --git a/src/cleveragents/resource/handlers/_base.py b/src/cleveragents/resource/handlers/_base.py new file mode 100644 index 0000000000..01770fdd01 --- /dev/null +++ b/src/cleveragents/resource/handlers/_base.py @@ -0,0 +1,99 @@ +"""Shared base class for resource handlers. + +Provides the common ``resolve`` logic used by both +:class:`GitCheckoutHandler` and :class:`FsDirectoryHandler`. +Subclasses set ``_default_strategy`` and ``_type_label`` to +customise behaviour. +""" + +from __future__ import annotations + +import logging +from typing import ClassVar, cast + +from cleveragents.domain.models.core.resource import Resource, SandboxStrategy +from cleveragents.infrastructure.sandbox.factory import SandboxStrategyStr +from cleveragents.infrastructure.sandbox.manager import SandboxManager +from cleveragents.tool.context import BoundResource + +logger = logging.getLogger(__name__) + + +class BaseResourceHandler: + """Base handler with shared resolve logic. + + Subclasses **must** set: + + * ``_default_strategy`` — the :class:`SandboxStrategy` to use when + the resource has no explicit override. + * ``_type_label`` — a human-readable label for error messages + (e.g. ``"git-checkout"``). + """ + + _default_strategy: ClassVar[SandboxStrategy] + _type_label: ClassVar[str] + + def resolve( + self, + *, + resource: Resource, + plan_id: str, + slot_name: str, + sandbox_manager: SandboxManager, + access: str = "read_only", + ) -> BoundResource: + """Resolve a resource into a sandboxed BoundResource. + + Args: + resource: A resource with ``location`` pointing to the + original path. + plan_id: The plan requesting the sandbox. + slot_name: Name of the tool resource slot being filled. + sandbox_manager: The sandbox lifecycle manager. + access: Access mode (``read_only`` or ``read_write``). + + Returns: + A :class:`BoundResource` with ``sandbox_path`` populated. + + Raises: + ValueError: If the resource has no location. + RuntimeError: If the sandbox was created but has no context. + """ + if not resource.location: + raise ValueError( + f"{self._type_label} resource '{resource.resource_id}' has no location" + ) + + strategy_raw = resource.sandbox_strategy or self._default_strategy + strategy_str = ( + strategy_raw.value if hasattr(strategy_raw, "value") else str(strategy_raw) + ) + + logger.debug( + "Resolving %s resource %s (location=%s, strategy=%s)", + self._type_label, + resource.resource_id, + resource.location, + strategy_str, + ) + + sandbox = sandbox_manager.get_or_create_sandbox( + plan_id=plan_id, + resource_id=resource.resource_id, + original_path=resource.location, + sandbox_strategy=cast(SandboxStrategyStr, strategy_str), + ) + + if sandbox.context is None: + raise RuntimeError( + f"Sandbox for resource '{resource.resource_id}' " + f"(plan={plan_id}) was created but has no context" + ) + + return BoundResource( + slot_name=slot_name, + resource_id=resource.resource_id, + resource_type=resource.resource_type_name, + sandbox_path=sandbox.context.sandbox_path, + access=access, + ) diff --git a/src/cleveragents/resource/handlers/fs_directory.py b/src/cleveragents/resource/handlers/fs_directory.py new file mode 100644 index 0000000000..78f1caac4a --- /dev/null +++ b/src/cleveragents/resource/handlers/fs_directory.py @@ -0,0 +1,35 @@ +"""Filesystem-directory resource handler. + +Resolves ``fs-directory`` resources into sandbox-backed +:class:`BoundResource` instances using the ``copy_on_write`` sandbox +strategy. + +The handler: + +1. Validates that the resource has a non-empty ``location``. +2. Determines the sandbox strategy: resource-level override takes + precedence over the default ``copy_on_write``. +3. Calls :meth:`SandboxManager.get_or_create_sandbox` to provision + (or reuse) an isolated copy-on-write directory. +4. Returns a :class:`BoundResource` with ``sandbox_path`` set to the + sandbox root. + +Based on: + - implementation_plan.md group M1.resource-handlers (L2254-L2271) + - Built-in type definition in resource_registry_service.py L99-123 +""" + +from __future__ import annotations + +from cleveragents.domain.models.core.resource import SandboxStrategy +from cleveragents.resource.handlers._base import BaseResourceHandler + + +class FsDirectoryHandler(BaseResourceHandler): + """Handler for ``fs-directory`` resource types. + + Provisions a copy-on-write sandbox for a local filesystem directory. + """ + + _default_strategy = SandboxStrategy.COPY_ON_WRITE + _type_label = "fs-directory" diff --git a/src/cleveragents/resource/handlers/git_checkout.py b/src/cleveragents/resource/handlers/git_checkout.py new file mode 100644 index 0000000000..169ce902e6 --- /dev/null +++ b/src/cleveragents/resource/handlers/git_checkout.py @@ -0,0 +1,36 @@ +"""Git-checkout resource handler. + +Resolves ``git-checkout`` resources into sandbox-backed +:class:`BoundResource` instances using the ``git_worktree`` sandbox +strategy (with fallback to ``copy_on_write``). + +The handler: + +1. Validates that the resource has a non-empty ``location``. +2. Determines the sandbox strategy: resource-level override takes + precedence over the default ``git_worktree``. +3. Calls :meth:`SandboxManager.get_or_create_sandbox` to provision + (or reuse) an isolated git worktree. +4. Returns a :class:`BoundResource` with ``sandbox_path`` set to the + sandbox root. + +Based on: + - implementation_plan.md group M1.resource-handlers (L2254-L2271) + - Built-in type definition in resource_registry_service.py L62-98 +""" + +from __future__ import annotations + +from cleveragents.domain.models.core.resource import SandboxStrategy +from cleveragents.resource.handlers._base import BaseResourceHandler + + +class GitCheckoutHandler(BaseResourceHandler): + """Handler for ``git-checkout`` resource types. + + Provisions a git-worktree sandbox (or copy-on-write fallback) + for a git repository checkout. + """ + + _default_strategy = SandboxStrategy.GIT_WORKTREE + _type_label = "git-checkout" diff --git a/src/cleveragents/resource/handlers/protocol.py b/src/cleveragents/resource/handlers/protocol.py new file mode 100644 index 0000000000..84ba4e5fc8 --- /dev/null +++ b/src/cleveragents/resource/handlers/protocol.py @@ -0,0 +1,78 @@ +"""Resource handler protocol for CleverAgents. + +Defines the :class:`ResourceHandler` protocol that all resource type +handlers must satisfy. A handler bridges a :class:`Resource` domain +object to sandbox provisioning by: + +1. Reading the resource's ``location`` (the original filesystem path). +2. Determining the sandbox strategy (from resource override or type default). +3. Calling :meth:`SandboxManager.get_or_create_sandbox` to provision an + isolated working directory. +4. Returning a :class:`BoundResource` with ``sandbox_path`` populated. + +Based on: + - implementation_plan.md group M1.resource-handlers (L2254-L2271) + - docs/specification.md Resource Handler architecture +""" + +from __future__ import annotations + +from typing import Protocol, runtime_checkable + +from cleveragents.domain.models.core.resource import Resource +from cleveragents.infrastructure.sandbox.manager import SandboxManager +from cleveragents.tool.context import BoundResource + + +@runtime_checkable +class ResourceHandler(Protocol): + """Protocol for resource type handlers. + + Each handler knows how to resolve a specific resource type into a + sandboxed working path. Implementations are registered via the + ``handler`` field on :class:`ResourceTypeSpec` using the + ``module:ClassName`` string format. + + Lifecycle:: + + handler = resolve_handler( + "cleveragents.resource.handlers.git_checkout:GitCheckoutHandler" + ) + bound = handler.resolve( + resource=resource, + plan_id="01ARZ3...", + slot_name="repo", + sandbox_manager=sandbox_manager, + access="read_write", + ) + # bound.sandbox_path is now populated + """ + + def resolve( + self, + *, + resource: Resource, + plan_id: str, + slot_name: str, + sandbox_manager: SandboxManager, + access: str = "read_only", + ) -> BoundResource: + """Resolve a resource into a sandbox-backed BoundResource. + + Args: + resource: The resource domain object to resolve. + plan_id: The plan requesting the sandbox. + slot_name: Name of the tool resource slot being filled. + sandbox_manager: The sandbox lifecycle manager. + access: Access mode (``read_only`` or ``read_write``). + + Returns: + A :class:`BoundResource` with ``sandbox_path`` populated + from the provisioned sandbox. + + Raises: + ValueError: If the resource lacks a location or has an + incompatible type. + SandboxError: If sandbox creation fails. + """ + ... diff --git a/src/cleveragents/resource/handlers/resolver.py b/src/cleveragents/resource/handlers/resolver.py new file mode 100644 index 0000000000..f8109cc0be --- /dev/null +++ b/src/cleveragents/resource/handlers/resolver.py @@ -0,0 +1,120 @@ +"""Dynamic handler resolution from ``module:ClassName`` strings. + +The :func:`resolve_handler` function takes a handler reference string +(as stored on :class:`ResourceTypeSpec`) and returns an instantiated +handler object that satisfies the :class:`ResourceHandler` protocol. + +Format:: + + "cleveragents.resource.handlers.git_checkout:GitCheckoutHandler" + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^ + module path class name + +The function uses :func:`importlib.import_module` for dynamic loading, +with a cache to avoid repeated imports. + +Based on: + - implementation_plan.md group M1.resource-handlers (L2254-L2271) +""" + +from __future__ import annotations + +import importlib +import logging +import threading +from typing import Any + +from cleveragents.resource.handlers.protocol import ResourceHandler + +logger = logging.getLogger(__name__) + +# Cache of already-resolved handler instances keyed by reference string +_handler_cache: dict[str, ResourceHandler] = {} +_cache_lock: threading.Lock = threading.Lock() + + +class HandlerResolutionError(Exception): + """Raised when a handler reference string cannot be resolved.""" + + +def resolve_handler(handler_ref: str) -> ResourceHandler: + """Resolve a handler reference string to an instance. + + Args: + handler_ref: A string in ``module.path:ClassName`` format. + + Returns: + An instantiated handler that satisfies :class:`ResourceHandler`. + + Raises: + HandlerResolutionError: If the reference is malformed, the + module cannot be imported, or the class does not exist. + """ + if not handler_ref or not handler_ref.strip(): + raise HandlerResolutionError("Handler reference must not be empty") + + handler_ref = handler_ref.strip() + + # Return cached instance if available + with _cache_lock: + if handler_ref in _handler_cache: + return _handler_cache[handler_ref] + + # Parse module:class format + if ":" not in handler_ref: + raise HandlerResolutionError( + f"Invalid handler reference format '{handler_ref}': " + "expected 'module.path:ClassName'" + ) + + module_path, class_name = handler_ref.rsplit(":", 1) + if not module_path or not class_name: + raise HandlerResolutionError( + f"Invalid handler reference '{handler_ref}': " + "both module path and class name are required" + ) + + # Import module + try: + module = importlib.import_module(module_path) + except ModuleNotFoundError as exc: + raise HandlerResolutionError( + f"Cannot import handler module '{module_path}': {exc}" + ) from exc + except Exception as exc: + raise HandlerResolutionError( + f"Error importing handler module '{module_path}': {exc}" + ) from exc + + # Get class + handler_cls: Any = getattr(module, class_name, None) + if handler_cls is None: + raise HandlerResolutionError( + f"Handler class '{class_name}' not found in module '{module_path}'" + ) + + # Instantiate + try: + instance = handler_cls() + except Exception as exc: + raise HandlerResolutionError( + f"Cannot instantiate handler '{handler_ref}': {exc}" + ) from exc + + # Validate protocol conformance + if not isinstance(instance, ResourceHandler): + raise HandlerResolutionError( + f"Handler '{handler_ref}' does not satisfy ResourceHandler protocol" + ) + + # Cache and return + with _cache_lock: + _handler_cache[handler_ref] = instance + logger.debug("Resolved handler: %s", handler_ref) + return instance + + +def clear_handler_cache() -> None: + """Clear the handler instance cache (useful for testing).""" + with _cache_lock: + _handler_cache.clear()