---
phase: 01-foundation-types-and-store-contract
plan: 02
type: execute
wave: 2
depends_on:
  - 01-01
files_modified:
  - crates/pmcp-tasks/src/domain/mod.rs
  - crates/pmcp-tasks/src/domain/record.rs
  - crates/pmcp-tasks/src/domain/variables.rs
  - crates/pmcp-tasks/src/store.rs
  - crates/pmcp-tasks/src/lib.rs
autonomous: true
requirements:
  - STOR-01
  - STOR-02
  - STOR-03
  - STOR-04
  - HNDL-01

must_haves:
  truths:
    - "TaskRecord struct holds protocol Task fields plus owner_id, variables, result, and request_method"
    - "TaskWithVariables wraps a Task and a HashMap of variables, injecting variables into _meta at serialization boundary"
    - "TaskStore async trait defines all 10 methods including atomic complete_with_result"
    - "TaskStore trait enforces configurable variable size limits in trait contract"
    - "ListTasksOptions and TaskPage types support cursor-based pagination"
  artifacts:
    - path: "crates/pmcp-tasks/src/domain/record.rs"
      provides: "TaskRecord struct with all fields"
      contains: "pub struct TaskRecord"
    - path: "crates/pmcp-tasks/src/domain/variables.rs"
      provides: "TaskWithVariables domain type with _meta injection"
      contains: "pub struct TaskWithVariables"
    - path: "crates/pmcp-tasks/src/store.rs"
      provides: "TaskStore async trait with all methods, ListTasksOptions, TaskPage, StoreConfig"
      contains: "pub trait TaskStore"
  key_links:
    - from: "crates/pmcp-tasks/src/domain/record.rs"
      to: "crates/pmcp-tasks/src/types/task.rs"
      via: "Uses Task and TaskStatus from wire types"
      pattern: "use crate::types::(Task|TaskStatus)"
    - from: "crates/pmcp-tasks/src/domain/variables.rs"
      to: "crates/pmcp-tasks/src/types/task.rs"
      via: "Uses Task wire type for to_wire_task conversion"
      pattern: "use crate::types::Task"
    - from: "crates/pmcp-tasks/src/store.rs"
      to: "crates/pmcp-tasks/src/domain/record.rs"
      via: "TaskStore methods return TaskRecord"
      pattern: "TaskRecord"
    - from: "crates/pmcp-tasks/src/store.rs"
      to: "crates/pmcp-tasks/src/error.rs"
      via: "TaskStore methods return Result<_, TaskError>"
      pattern: "Result<.*TaskError>"
---

<objective>
Create domain types (TaskRecord, TaskWithVariables) and the TaskStore async trait with all storage contract methods, supporting types, and configuration.

Purpose: The TaskStore trait is the contract that all backends (in-memory in Phase 2, DynamoDB in Phase 4) must implement. TaskRecord is the store's internal representation. TaskWithVariables is the PMCP extension that injects variables into _meta for client responses. These domain types separate internal concerns from wire-format concerns.

Output: Complete domain module with TaskRecord and TaskWithVariables, and the TaskStore trait with all 10 methods, pagination types, and store configuration.
</objective>

<execution_context>
@./.claude/get-shit-done/workflows/execute-plan.md
@./.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-foundation-types-and-store-contract/01-CONTEXT.md
@.planning/phases/01-foundation-types-and-store-contract/01-RESEARCH.md
@.planning/phases/01-foundation-types-and-store-contract/01-01-SUMMARY.md
@crates/pmcp-tasks/src/lib.rs
@crates/pmcp-tasks/src/types/task.rs
@crates/pmcp-tasks/src/error.rs
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create TaskRecord and TaskWithVariables domain types</name>
  <files>
    crates/pmcp-tasks/src/domain/mod.rs
    crates/pmcp-tasks/src/domain/record.rs
    crates/pmcp-tasks/src/domain/variables.rs
    crates/pmcp-tasks/src/lib.rs (update pub mod domain from stub to real)
  </files>
  <action>
1. Update `crates/pmcp-tasks/src/lib.rs` to make `pub mod domain` a real module (remove stub if present). Ensure `pub use domain::*` re-exports.

2. Create `crates/pmcp-tasks/src/domain/mod.rs`:
   - Declare and re-export: `pub mod record; pub mod variables;`
   - `pub use record::*; pub use variables::*;`

3. Create `crates/pmcp-tasks/src/domain/record.rs` (STOR-04):
   - **TaskRecord struct** with public fields:
     - `task: Task` (the wire-format task)
     - `owner_id: String` (who created this task)
     - `variables: HashMap<String, serde_json::Value>` (shared variable store)
     - `result: Option<serde_json::Value>` (the operation result, set on completion)
     - `request_method: String` (e.g., "tools/call" -- which MCP method created this task)
     - `expires_at: Option<chrono::DateTime<chrono::Utc>>` (computed from TTL for efficient expiry checks)
   - Derive: Debug, Clone
   - Constructor: `TaskRecord::new(owner_id: String, request_method: String, ttl: Option<u64>) -> Self`
     - Generates task_id via `uuid::Uuid::new_v4().to_string()`
     - Sets status to Working, created_at and last_updated_at to current UTC ISO 8601 string
     - Computes expires_at from ttl (if Some, now + Duration::from_millis(ttl))
     - Initializes variables as empty HashMap, result as None
   - Method: `is_expired(&self) -> bool` -- checks expires_at against Utc::now()
   - Method: `to_wire_task(&self) -> Task` -- returns clone of self.task
   - Method: `to_wire_task_with_variables(&self) -> Task` -- returns task clone with variables injected into `_meta` field (per CONTEXT.md locked decision: variables at top level of _meta)

4. Create `crates/pmcp-tasks/src/domain/variables.rs` (HNDL-01):
   - **TaskWithVariables struct**:
     - `task: Task` (wire type)
     - `variables: HashMap<String, serde_json::Value>` (the variable store)
   - Derive: Debug, Clone
   - Method: `to_wire_task(&self) -> Task` -- injects variables into task._meta. For each (key, value) in variables, insert into _meta map. If _meta is None, create new Map. Variables go at top level of _meta (NOT nested under a PMCP key -- per locked decision).
   - Method: `from_record(record: &TaskRecord) -> Self` -- constructs from a TaskRecord, copying task and variables.
   - Note: TaskWithVariables is NOT serialized directly. It is a domain convenience type. The wire task (with _meta populated) is what gets serialized.

5. IMPORTANT per locked decisions:
   - Variables placed at top level of `_meta` (not nested under a PMCP-specific key)
   - Bidirectional read/write: both client and server can read and write variables
   - Store implementors need full access to TaskRecord public fields
  </action>
  <verify>
    Run `cargo check --package pmcp-tasks` -- zero errors.
    Run `cargo clippy --package pmcp-tasks -- -D warnings` -- zero warnings.
  </verify>
  <done>
    TaskRecord struct exists with all required fields (task, owner_id, variables, result, request_method, expires_at). TaskRecord::new() generates UUIDv4 task_id and computes expiry. TaskWithVariables exists and can inject variables into _meta at top level. Both types derive Debug and Clone.
  </done>
</task>

<task type="auto">
  <name>Task 2: Create TaskStore trait and supporting pagination types</name>
  <files>
    crates/pmcp-tasks/src/store.rs
    crates/pmcp-tasks/src/lib.rs (update pub mod store from stub to real)
  </files>
  <action>
1. Update `crates/pmcp-tasks/src/lib.rs` to make `pub mod store` a real module (remove stub if present). Add `pub use store::*` re-export.

2. Create `crates/pmcp-tasks/src/store.rs` with:

   **StoreConfig struct** (STOR-03):
   - `max_variable_size_bytes: usize` (default: 1MB = 1_048_576)
   - `default_ttl_ms: Option<u64>` (default: 3_600_000 = 1 hour)
   - `max_ttl_ms: Option<u64>` (default: 86_400_000 = 24 hours)
   - Derive: Debug, Clone
   - `Default` impl with the above defaults
   - Doc comment: "Configuration for variable size limits and TTL enforcement. Applied at the trait level across all backends."

   **ListTasksOptions struct**:
   - `owner_id: String` (tasks scoped to this owner)
   - `cursor: Option<String>` (for pagination)
   - `limit: Option<usize>` (page size, default backend-dependent)
   - Derive: Debug, Clone

   **TaskPage struct**:
   - `tasks: Vec<TaskRecord>` (the page of results)
   - `next_cursor: Option<String>` (None if no more results)
   - Derive: Debug, Clone

   **TaskStore async trait** (STOR-01, STOR-02):
   - Use `#[async_trait]` from async-trait crate
   - Bound: `Send + Sync`
   - Methods:

     ```rust
     /// Create a new task in working state.
     async fn create(
         &self,
         owner_id: &str,
         request_method: &str,
         ttl: Option<u64>,
     ) -> Result<TaskRecord, TaskError>;

     /// Get a task by ID. Returns Expired error if task is past TTL.
     async fn get(
         &self,
         task_id: &str,
         owner_id: &str,
     ) -> Result<TaskRecord, TaskError>;

     /// Transition task to a new status. Validates state machine.
     async fn update_status(
         &self,
         task_id: &str,
         owner_id: &str,
         new_status: TaskStatus,
         status_message: Option<String>,
     ) -> Result<TaskRecord, TaskError>;

     /// Merge variables into task. Null values delete keys.
     async fn set_variables(
         &self,
         task_id: &str,
         owner_id: &str,
         variables: HashMap<String, Value>,
     ) -> Result<TaskRecord, TaskError>;

     /// Store the operation result for a task.
     async fn set_result(
         &self,
         task_id: &str,
         owner_id: &str,
         result: Value,
     ) -> Result<(), TaskError>;

     /// Retrieve the stored result. Returns NotReady if not terminal.
     async fn get_result(
         &self,
         task_id: &str,
         owner_id: &str,
     ) -> Result<Value, TaskError>;

     /// Atomically transition to terminal status AND store result.
     /// If either operation fails, neither is applied.
     async fn complete_with_result(
         &self,
         task_id: &str,
         owner_id: &str,
         status: TaskStatus,
         status_message: Option<String>,
         result: Value,
     ) -> Result<TaskRecord, TaskError>;

     /// List tasks for an owner with cursor-based pagination.
     async fn list(
         &self,
         options: ListTasksOptions,
     ) -> Result<TaskPage, TaskError>;

     /// Cancel a non-terminal task. Returns InvalidTransition for terminal tasks.
     async fn cancel(
         &self,
         task_id: &str,
         owner_id: &str,
     ) -> Result<TaskRecord, TaskError>;

     /// Remove expired tasks from storage. Returns count removed.
     async fn cleanup_expired(&self) -> Result<usize, TaskError>;

     /// Get the store configuration (variable limits, TTL defaults).
     fn config(&self) -> &StoreConfig;
     ```

   - Add comprehensive doc comments on each method explaining:
     - What it does
     - Error conditions (which TaskError variants it can return)
     - Atomicity guarantees (especially for complete_with_result)
   - The `config()` method is sync (not async) since it returns a reference to configuration.
  </action>
  <verify>
    Run `cargo check --package pmcp-tasks` -- zero errors.
    Run `cargo clippy --package pmcp-tasks -- -D warnings` -- zero warnings.
    Run `cargo doc --package pmcp-tasks --no-deps` -- documentation builds, all public items documented.
  </verify>
  <done>
    TaskStore async trait exists with all 11 methods (create, get, update_status, set_variables, set_result, get_result, complete_with_result, list, cancel, cleanup_expired, config). StoreConfig provides configurable variable size limits. ListTasksOptions and TaskPage support cursor-based pagination. All methods have doc comments explaining behavior and error conditions.
  </done>
</task>

</tasks>

<verification>
1. `cargo check --package pmcp-tasks` compiles with domain types and store trait
2. `cargo clippy --package pmcp-tasks -- -D warnings` passes
3. TaskRecord has all fields: task, owner_id, variables, result, request_method, expires_at
4. TaskWithVariables injects variables into _meta at top level (not nested)
5. TaskStore trait has all 11 methods with correct signatures
6. StoreConfig has max_variable_size_bytes, default_ttl_ms, max_ttl_ms
</verification>

<success_criteria>
- TaskRecord struct compiles with all required fields and constructor
- TaskWithVariables correctly injects variables into _meta
- TaskStore trait is defined with all methods including atomic complete_with_result
- Variable size limit configuration is part of the store contract
- All public types have documentation
</success_criteria>

<output>
After completion, create `.planning/phases/01-foundation-types-and-store-contract/01-02-SUMMARY.md`
</output>
