---
phase: 02-in-memory-backend-and-owner-security
plan: 02
type: execute
wave: 2
depends_on:
  - 02-01
files_modified:
  - crates/pmcp-tasks/src/context.rs
  - crates/pmcp-tasks/src/lib.rs
  - crates/pmcp-tasks/tests/context_tests.rs
autonomous: true
requirements:
  - HNDL-04
  - HNDL-05
  - HNDL-06
  - TEST-03

must_haves:
  truths:
    - "TaskContext is Clone + Send + Sync and wraps Arc<dyn TaskStore>"
    - "TaskContext provides typed variable accessors (get_string, get_i64, get_f64, get_bool, get_typed<T>)"
    - "TaskContext provides status transition convenience methods (complete, fail, require_input)"
    - "TaskContext complete() uses store's atomic complete_with_result for status + result in one operation"
    - "TaskContext set_variable sets a single variable, set_variables merges a map, variables() returns all"
    - "Integration tests verify variable CRUD, status transitions, and complete_with_result through TaskContext"
  artifacts:
    - path: "crates/pmcp-tasks/src/context.rs"
      provides: "TaskContext ergonomic wrapper for tool handlers"
      contains: "pub struct TaskContext"
      exports: ["TaskContext"]
    - path: "crates/pmcp-tasks/tests/context_tests.rs"
      provides: "TEST-03 integration tests for TaskContext behavior"
      contains: "mod context_tests"
  key_links:
    - from: "crates/pmcp-tasks/src/context.rs"
      to: "crates/pmcp-tasks/src/store/mod.rs"
      via: "Arc<dyn TaskStore> field for all operations"
      pattern: "Arc<dyn TaskStore>"
    - from: "crates/pmcp-tasks/src/context.rs"
      to: "crates/pmcp-tasks/src/store/memory.rs"
      via: "tests use InMemoryTaskStore as the backing store"
      pattern: "InMemoryTaskStore"
    - from: "crates/pmcp-tasks/tests/context_tests.rs"
      to: "crates/pmcp-tasks/src/context.rs"
      via: "exercises all TaskContext methods against InMemoryTaskStore"
      pattern: "TaskContext::new"
---

<objective>
Implement TaskContext -- the ergonomic wrapper that tool handlers use to interact with tasks.

Purpose: Tool handlers should not need to know about task IDs, owner IDs, or store internals. TaskContext scopes all operations to a single task and provides typed variable accessors and convenient status transition methods. This is the primary API surface for server developers writing task-aware tools.

Output: `context.rs` with the TaskContext struct and `context_tests.rs` with TEST-03 integration tests covering variable CRUD, status transitions, and complete_with_result.
</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/02-in-memory-backend-and-owner-security/02-CONTEXT.md
@.planning/phases/02-in-memory-backend-and-owner-security/02-RESEARCH.md
@.planning/phases/02-in-memory-backend-and-owner-security/02-01-SUMMARY.md

Source files:
@crates/pmcp-tasks/src/store/mod.rs (TaskStore trait, StoreConfig)
@crates/pmcp-tasks/src/store/memory.rs (InMemoryTaskStore -- from Plan 01)
@crates/pmcp-tasks/src/domain/record.rs (TaskRecord)
@crates/pmcp-tasks/src/error.rs (TaskError)
@crates/pmcp-tasks/src/types/task.rs (TaskStatus)
@crates/pmcp-tasks/src/lib.rs (re-exports)
</context>

<tasks>

<task type="auto">
  <name>Task 1: Implement TaskContext ergonomic wrapper in context.rs</name>
  <files>crates/pmcp-tasks/src/context.rs, crates/pmcp-tasks/src/lib.rs</files>
  <action>
Create `crates/pmcp-tasks/src/context.rs` with:

**TaskContext struct** (derive Clone):
```rust
#[derive(Clone)]
pub struct TaskContext {
    store: Arc<dyn TaskStore>,
    task_id: String,
    owner_id: String,
}
```

This is `Clone + Send + Sync` automatically because `Arc<dyn TaskStore>` is Send+Sync (TaskStore: Send + Sync) and String is Send+Sync. Per HNDL-06.

**Constructor:**
```rust
pub fn new(store: Arc<dyn TaskStore>, task_id: String, owner_id: String) -> Self
```

**Accessor methods:**
- `pub fn task_id(&self) -> &str` -- returns the task ID
- `pub fn owner_id(&self) -> &str` -- returns the owner ID

**Task retrieval:**
- `pub async fn get(&self) -> Result<TaskRecord, TaskError>` -- delegates to store.get()

**Variable accessors (per HNDL-04):**
- `pub async fn get_variable(&self, key: &str) -> Result<Option<Value>, TaskError>` -- get raw JSON variable
- `pub async fn get_string(&self, key: &str) -> Result<Option<String>, TaskError>` -- typed: uses Value::as_str
- `pub async fn get_i64(&self, key: &str) -> Result<Option<i64>, TaskError>` -- typed: uses Value::as_i64
- `pub async fn get_f64(&self, key: &str) -> Result<Option<f64>, TaskError>` -- typed: uses Value::as_f64
- `pub async fn get_bool(&self, key: &str) -> Result<Option<bool>, TaskError>` -- typed: uses Value::as_bool
- `pub async fn get_typed<T: serde::de::DeserializeOwned>(&self, key: &str) -> Result<Option<T>, TaskError>` -- generic typed accessor using serde_json::from_value

Each typed accessor: calls `self.store.get(&self.task_id, &self.owner_id).await?`, looks up key in `record.variables`, applies type conversion. Returns `Ok(None)` if key absent or type mismatch (not an error).

**Variable mutators (per HNDL-04):**
- `pub async fn set_variable(&self, key: impl Into<String>, value: Value) -> Result<TaskRecord, TaskError>` -- wraps single key into HashMap, delegates to store.set_variables()
- `pub async fn set_variables(&self, variables: HashMap<String, Value>) -> Result<TaskRecord, TaskError>` -- direct delegation to store.set_variables()
- `pub async fn variables(&self) -> Result<HashMap<String, Value>, TaskError>` -- calls get(), returns record.variables.clone()
- `pub async fn delete_variable(&self, key: impl Into<String>) -> Result<TaskRecord, TaskError>` -- sets key to Value::Null (null-deletion semantics from HNDL-03)

**Status transition convenience methods (per HNDL-05):**
- `pub async fn complete(&self, result: Value) -> Result<TaskRecord, TaskError>` -- delegates to `store.complete_with_result(task_id, owner_id, TaskStatus::Completed, None, result)`. Uses the atomic complete_with_result method per the store trait's atomicity guarantee.
- `pub async fn fail(&self, message: impl Into<String>) -> Result<TaskRecord, TaskError>` -- delegates to `store.update_status(task_id, owner_id, TaskStatus::Failed, Some(message.into()))`
- `pub async fn require_input(&self, message: impl Into<String>) -> Result<TaskRecord, TaskError>` -- delegates to `store.update_status(task_id, owner_id, TaskStatus::InputRequired, Some(message.into()))`
- `pub async fn resume(&self) -> Result<TaskRecord, TaskError>` -- delegates to `store.update_status(task_id, owner_id, TaskStatus::Working, None)` (for resuming from input_required)
- `pub async fn cancel(&self) -> Result<TaskRecord, TaskError>` -- delegates to `store.cancel(task_id, owner_id)`

Include comprehensive rustdoc on the struct and all public methods. Add examples in doc comments showing typical usage in a tool handler.

**Update `crates/pmcp-tasks/src/lib.rs`:**
- Add `pub mod context;`
- Add `pub use context::TaskContext;` to re-exports
  </action>
  <verify>
`cargo check --package pmcp-tasks` succeeds.
`cargo clippy --package pmcp-tasks -- -D warnings` reports zero warnings.
  </verify>
  <done>
TaskContext is Clone + Send + Sync, wraps Arc&lt;dyn TaskStore&gt;, provides typed variable accessors (get_string, get_i64, get_f64, get_bool, get_typed&lt;T&gt;), variable mutators (set_variable, set_variables, delete_variable, variables), and status transition methods (complete, fail, require_input, resume, cancel). Re-exported from crate root.
  </done>
</task>

<task type="auto">
  <name>Task 2: Write TEST-03 integration tests for TaskContext behavior</name>
  <files>crates/pmcp-tasks/tests/context_tests.rs</files>
  <action>
Create `crates/pmcp-tasks/tests/context_tests.rs` as an integration test file (TEST-03).

Use `InMemoryTaskStore` as the backing store. All tests are `#[tokio::test]`.

**Helper function:**
```rust
fn create_store_and_context() -> (Arc<InMemoryTaskStore>, ...) {
    // Create store with allow_anonymous: true for test convenience
    // Create a task, return store + TaskContext
}
```

**Variable CRUD tests:**
- `test_set_and_get_string_variable` -- set a string, get_string returns it
- `test_set_and_get_i64_variable` -- set an i64, get_i64 returns it
- `test_set_and_get_f64_variable` -- set f64, get_f64 returns it
- `test_set_and_get_bool_variable` -- set bool, get_bool returns it
- `test_get_typed_struct` -- set a JSON object, get_typed::<CustomStruct> returns it
- `test_get_variable_returns_none_for_missing_key` -- key does not exist returns Ok(None)
- `test_get_string_returns_none_for_wrong_type` -- stored as i64, get_string returns Ok(None)
- `test_set_variables_merges_multiple` -- set_variables with multiple keys, verify all present
- `test_delete_variable_via_null` -- delete_variable removes the key from the store
- `test_variables_returns_all` -- set multiple, variables() returns complete map

**Status transition tests:**
- `test_complete_transitions_to_completed_with_result` -- complete(value) sets status=completed and stores result
- `test_fail_transitions_to_failed_with_message` -- fail("reason") sets status=failed, message="reason"
- `test_require_input_transitions_to_input_required` -- require_input("prompt") sets status=input_required
- `test_resume_transitions_back_to_working` -- from input_required, resume() goes to working
- `test_cancel_transitions_to_cancelled` -- cancel() sets status=cancelled
- `test_complete_is_atomic` -- after complete(), both status and result are set (get() shows completed, get_result shows value)
- `test_invalid_transition_returns_error` -- complete() then fail() returns InvalidTransition error

**TaskContext identity tests:**
- `test_task_id_returns_correct_id` -- task_id() matches the created task
- `test_owner_id_returns_correct_owner` -- owner_id() matches the owner used
- `test_clone_shares_store` -- cloned context operates on same task

Organize tests using `mod` blocks for clarity: `mod variable_tests`, `mod transition_tests`, `mod identity_tests`.

Use `pretty_assertions::assert_eq` for better test output. Import it at the top of each mod block (not file-level, per Phase 1 convention to avoid ambiguity).
  </action>
  <verify>
`cargo test --package pmcp-tasks --test context_tests` passes all tests.
`cargo test --package pmcp-tasks` passes all tests (no regressions).
  </verify>
  <done>
context_tests.rs contains 20+ integration tests covering variable CRUD with typed accessors, null-deletion semantics, status transition convenience methods, atomic complete_with_result, invalid transition errors, and TaskContext identity/clone behavior. All tests pass against InMemoryTaskStore.
  </done>
</task>

</tasks>

<verification>
```bash
# TaskContext compiles
cargo check --package pmcp-tasks

# All context tests pass
cargo test --package pmcp-tasks --test context_tests

# No regressions
cargo test --package pmcp-tasks

# Zero clippy warnings
cargo clippy --package pmcp-tasks -- -D warnings
```
</verification>

<success_criteria>
1. TaskContext is Clone + Send + Sync with Arc&lt;dyn TaskStore&gt;
2. Typed variable accessors work correctly (get_string, get_i64, get_f64, get_bool, get_typed)
3. Status transition methods delegate correctly to the store
4. complete() uses atomic complete_with_result
5. All context integration tests pass
6. All Phase 1 + Plan 01 tests still pass
</success_criteria>

<output>
After completion, create `.planning/phases/02-in-memory-backend-and-owner-security/02-02-SUMMARY.md`
</output>
