---
phase: 06-structured-handoff-and-client-continuation
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/server/workflow/task_prompt_handler.rs
autonomous: true
requirements: [HAND-01, HAND-02, HAND-03]

must_haves:
  truths:
    - "When execution pauses, the prompt reply includes a final assistant message narrating remaining steps with tool names, resolved arguments, and guidance"
    - "The handoff does NOT repeat completed steps (they are already in the conversation trace as tool-call/result message pairs)"
    - "Task ID appears only in _meta, never in the narrative text"
    - "Arguments that cannot be resolved use placeholder syntax: <output from step_name>"
    - "Step guidance text is included when the step has a guidance field"
    - "The _meta JSON block plus the narrative assistant message together form the hybrid format"
  artifacts:
    - path: "src/server/workflow/task_prompt_handler.rs"
      provides: "build_handoff_message method and build_placeholder_args helper"
      contains: "build_handoff_message"
  key_links:
    - from: "TaskWorkflowPromptHandler::handle()"
      to: "build_handoff_message()"
      via: "called when pause_reason.is_some() before returning GetPromptResult"
      pattern: "build_handoff_message"
    - from: "build_handoff_message()"
      to: "resolve_tool_parameters()"
      via: "attempts resolution, falls back to placeholders on Err"
      pattern: "resolve_tool_parameters.*Err"
---

<objective>
Add structured handoff message generation to TaskWorkflowPromptHandler.

Purpose: When execution pauses partway through a workflow, the prompt reply must tell the LLM client what happened and what to do next. The handoff is a final assistant message appended to the messages vec, narrating remaining steps with tool names, resolved arguments (or placeholders), and guidance text. Combined with the existing _meta JSON block (Phase 5), this creates the hybrid format: machine-parseable JSON for programmatic clients + natural language for LLM clients.

Output: Modified `task_prompt_handler.rs` with handoff message generation, placeholder argument building, and comprehensive unit tests.
</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/06-structured-handoff-and-client-continuation/06-CONTEXT.md
@.planning/phases/06-structured-handoff-and-client-continuation/06-RESEARCH.md
@.planning/phases/05-partial-execution-engine/05-02-SUMMARY.md
@src/server/workflow/task_prompt_handler.rs
@src/server/workflow/prompt_handler.rs
@src/server/workflow/workflow_step.rs
</context>

<tasks>

<task type="auto">
  <name>Task 1: Handoff message generation with argument resolution and placeholder fallback</name>
  <files>src/server/workflow/task_prompt_handler.rs</files>
  <action>
Add two methods to `TaskWorkflowPromptHandler`:

1. `build_placeholder_args(&self, step: &WorkflowStep, _ctx: &ExecutionContext) -> String`
   - Iterates step.arguments() and for each DataSource:
     - `DataSource::PromptArg(name)` -> use the actual prompt arg value from `args` if available, else `"<prompt arg {name}>"`
     - `DataSource::StepOutput { step: binding, .. }` -> `"<output from {binding}>"`
     - `DataSource::Constant(val)` -> serialize val to string
     - `DataSource::Field { source, field }` -> recursively resolve source, extract field, with placeholder fallback
   - Returns a JSON-formatted string of the placeholder argument map

2. `build_handoff_message(&self, step_statuses: &[StepStatus], pause_reason: &PauseReason, args: &HashMap<String, String>, execution_context: &ExecutionContext) -> PromptMessage`
   - Builds a single `PromptMessage { role: Role::Assistant, content: MessageContent::Text { text } }`
   - The text has two sections:

   **Section 1 - What happened (from pause_reason):**
   - `PauseReason::ToolError { failed_step, error, retryable, .. }` -> "Step '{failed_step}' failed: {error}." + if retryable: " This step is retryable."
   - `PauseReason::UnresolvableParams { blocked_step, missing_param, .. }` -> "Could not resolve parameter '{missing_param}' for step '{blocked_step}'."
   - `PauseReason::SchemaMismatch { blocked_step, missing_fields, .. }` -> "Step '{blocked_step}' has missing required fields: {fields}."
   - `PauseReason::UnresolvedDependency { blocked_step, missing_output, producing_step, .. }` -> "Step '{blocked_step}' depends on output '{missing_output}' from step '{producing_step}', which did not complete."

   **Section 2 - Remaining steps:**
   - "\n\nTo continue the workflow, make these tool calls:\n\n"
   - For each step where `step_statuses[idx] == StepStatus::Pending`:
     - Get tool name from `step.tool().map(|t| t.name().to_string()).unwrap_or("unknown")`
     - Attempt `self.inner.resolve_tool_parameters(step, args, execution_context)`:
       - On Ok(resolved): `serde_json::to_string(&resolved)` for args display
       - On Err(_): call `self.build_placeholder_args(step, execution_context)` for placeholder display
     - Format: "{n}. Call {tool_name} with {args_str}\n"
     - If `step.guidance()` returns Some(guidance_template): substitute args via `WorkflowPromptHandler::substitute_arguments(guidance_template, args)` and append "   Note: {guidance_text}\n"
   - CRITICAL: Do NOT include the task ID in the narrative text (it is only in _meta per locked decision)
   - CRITICAL: Do NOT re-summarize completed steps (they are already in the conversation trace)
   - Failed steps that also need retrying SHOULD appear in the remaining steps list (their status is Failed, not Pending, but if the pause is ToolError with retryable=true, include the failed step as the first item)

Then modify the `handle()` method:
   - After building `step_statuses` and `pause_reason` (around line 670, after the step loop ends), before the batch write section:
   - If `pause_reason.is_some()`, call `build_handoff_message()` and push the result onto `messages`
   - This must happen BEFORE the `_meta` and result construction, so the handoff message is the last assistant message in the conversation trace

Testing: Add these unit tests to the existing `mod tests`:

1. `handoff_message_tool_error_retryable` - ToolError with retryable=true produces correct text with retryable note and lists remaining steps
2. `handoff_message_unresolvable_params` - UnresolvableParams produces correct text with parameter name
3. `handoff_message_unresolved_dependency` - UnresolvedDependency mentions producing step
4. `handoff_message_schema_mismatch` - SchemaMismatch lists missing fields
5. `handoff_message_no_task_id_in_text` - Verify the narrative text does NOT contain any task ID
6. `placeholder_args_step_output` - build_placeholder_args produces "<output from {binding}>" for StepOutput data sources
7. `handoff_includes_guidance` - When step has guidance text, the handoff includes "Note: {guidance}" after the step instruction

Note: For testing build_handoff_message, you can construct a TaskWorkflowPromptHandler with a mock task router and minimal workflow, then call the method directly. The method does not need async execution since it only reads step metadata and calls resolve_tool_parameters (which is sync).
  </action>
  <verify>
Run `cargo test --package pmcp --lib workflow::task_prompt_handler` -- all existing tests plus 7 new tests pass. Run `cargo clippy --package pmcp -- -D warnings` -- zero warnings. Run `cargo fmt --check` -- clean formatting.
  </verify>
  <done>
When a workflow pauses, the prompt reply includes a final assistant message with: (1) description of why execution paused, (2) numbered list of remaining steps with tool names, resolved or placeholder arguments, and guidance text. Task ID never appears in the narrative. Completed steps are not re-summarized. The existing _meta JSON block + this narrative message together form the hybrid handoff format.
  </done>
</task>

</tasks>

<verification>
1. `cargo test --package pmcp --lib workflow::task_prompt_handler` -- all tests pass (existing + 7 new)
2. `cargo clippy --package pmcp -- -D warnings` -- zero warnings
3. `cargo test --package pmcp --lib` -- full lib tests pass (no regression)
4. Manual review: handoff narrative text contains no task ID references
5. Manual review: handoff narrative lists only remaining (Pending) steps, not completed ones
</verification>

<success_criteria>
- build_handoff_message produces correct narrative for all 4 PauseReason variants
- Placeholder arguments use `<output from {binding}>` syntax for unresolvable step outputs
- Guidance text is included when step has guidance field
- Task ID never appears in narrative text
- Completed steps are not re-summarized in the handoff
- handle() appends handoff message to messages vec when pause_reason is present
- All existing workflow tests pass unchanged (backward compatibility)
</success_criteria>

<output>
After completion, create `.planning/phases/06-structured-handoff-and-client-continuation/06-01-SUMMARY.md`
</output>
