---
phase: 08.1-agent-output-schema
plan: 01
type: execute
wave: 1
depends_on: []
files_modified: [src/agent/schema.ts, src/agent/manager.ts, src/agent/types.ts]
autonomous: true
---

<objective>
Define structured agent output schema and update ClaudeAgentManager to use `--json-schema` flag for validated output parsing.

Purpose: Replace broken AskUserQuestion detection with explicit agent status signaling via discriminated union schema.
Output: Zod schema for agent output, updated ClaudeAgentManager with proper question/resume flow.
</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

@src/agent/manager.ts
@src/agent/types.ts
@src/events/types.ts
</context>

<tasks>

<task type="auto">
  <name>Task 1: Define agent output schema with Zod</name>
  <files>src/agent/schema.ts</files>
  <action>
Create new file `src/agent/schema.ts` with discriminated union schema:

```typescript
import { z } from 'zod';

// Option for questions
const optionSchema = z.object({
  label: z.string(),
  description: z.string().optional(),
});

// Discriminated union for agent output
export const agentOutputSchema = z.discriminatedUnion('status', [
  // Agent completed successfully
  z.object({
    status: z.literal('done'),
    result: z.string(),
    filesModified: z.array(z.string()).optional(),
  }),

  // Agent needs user input to continue
  z.object({
    status: z.literal('question'),
    question: z.string(),
    options: z.array(optionSchema).optional(),
    multiSelect: z.boolean().optional(),
  }),

  // Agent hit unrecoverable error
  z.object({
    status: z.literal('unrecoverable_error'),
    error: z.string(),
    attempted: z.string().optional(),
  }),
]);

export type AgentOutput = z.infer<typeof agentOutputSchema>;

// JSON Schema for --json-schema flag (convert Zod to JSON Schema)
export const agentOutputJsonSchema = {
  type: 'object',
  oneOf: [
    {
      properties: {
        status: { const: 'done' },
        result: { type: 'string' },
        filesModified: { type: 'array', items: { type: 'string' } },
      },
      required: ['status', 'result'],
    },
    {
      properties: {
        status: { const: 'question' },
        question: { type: 'string' },
        options: {
          type: 'array',
          items: {
            type: 'object',
            properties: {
              label: { type: 'string' },
              description: { type: 'string' },
            },
            required: ['label'],
          },
        },
        multiSelect: { type: 'boolean' },
      },
      required: ['status', 'question'],
    },
    {
      properties: {
        status: { const: 'unrecoverable_error' },
        error: { type: 'string' },
        attempted: { type: 'string' },
      },
      required: ['status', 'error'],
    },
  ],
};
```

Export both the Zod schema (for runtime validation) and JSON schema string (for CLI flag).
  </action>
  <verify>npx tsc --noEmit src/agent/schema.ts</verify>
  <done>Schema file created with discriminated union and JSON schema export</done>
</task>

<task type="auto">
  <name>Task 2: Update ClaudeAgentManager to use schema and handle question flow</name>
  <files>src/agent/manager.ts, src/agent/types.ts</files>
  <action>
Update ClaudeAgentManager:

1. **Import schema:**
   ```typescript
   import { agentOutputSchema, agentOutputJsonSchema } from './schema.js';
   ```

2. **Update spawn() to pass --json-schema:**
   ```typescript
   const subprocess = execa(
     'claude',
     [
       '-p', prompt,
       '--output-format', 'json',
       '--json-schema', JSON.stringify(agentOutputJsonSchema),
     ],
     { cwd: cwd ?? worktree.path, detached: true, stdio: ['ignore', 'pipe', 'pipe'] }
   );
   ```

3. **Update handleAgentCompletion() to parse discriminated union:**

   Replace the current ClaudeCliResult parsing with:
   ```typescript
   const cliResult = JSON.parse(stdout as string);

   // Store session_id for resume capability
   if (cliResult.session_id) {
     await this.repository.updateSessionId(agentId, cliResult.session_id);
   }

   // Parse the agent's structured output from result field
   const agentOutput = agentOutputSchema.parse(JSON.parse(cliResult.result));

   switch (agentOutput.status) {
     case 'done':
       // Success path - existing logic
       active.result = { success: true, message: agentOutput.result, filesModified: agentOutput.filesModified };
       await this.repository.updateStatus(agentId, 'idle');
       // Emit agent:stopped event
       break;

     case 'question':
       // Question path - agent needs input
       await this.repository.updateStatus(agentId, 'waiting_for_input');
       // Store question metadata for later retrieval
       active.pendingQuestion = agentOutput;
       // Emit agent:waiting event with structured question
       break;

     case 'unrecoverable_error':
       // Error path
       active.result = { success: false, message: agentOutput.error };
       await this.repository.updateStatus(agentId, 'crashed');
       // Emit agent:crashed event
       break;
   }
   ```

4. **Update ActiveAgent interface:**
   ```typescript
   interface ActiveAgent {
     subprocess: ResultPromise;
     result?: AgentResult;
     pendingQuestion?: { question: string; options?: Array<{label: string; description?: string}>; multiSelect?: boolean };
   }
   ```

5. **Update resume() to use same session_id:**
   - Already uses `--resume` flag with stored session_id ✓
   - Ensure prompt passed is the user's answer to the question

6. **Add method to get pending question:**
   ```typescript
   async getPendingQuestion(agentId: string): Promise<{question: string; options?: ...} | null> {
     const active = this.activeAgents.get(agentId);
     return active?.pendingQuestion ?? null;
   }
   ```

7. **Update AgentManager interface in types.ts:**
   Add getPendingQuestion method signature.

8. **Remove the hacky string matching** in handleAgentError() for 'waiting for input' detection - no longer needed.
  </action>
  <verify>npm run build && npm run typecheck</verify>
  <done>ClaudeAgentManager uses --json-schema, parses discriminated union, handles question/resume flow correctly</done>
</task>

<task type="auto">
  <name>Task 3: Update AgentWaitingEvent to include structured question data</name>
  <files>src/events/types.ts</files>
  <action>
Update AgentWaitingEvent payload to include structured question data:

```typescript
export interface AgentWaitingEvent extends BaseEvent {
  type: 'agent:waiting';
  payload: {
    agentId: string;
    name: string;
    taskId: string;
    sessionId: string;
    question: string;
    options?: Array<{ label: string; description?: string }>;
    multiSelect?: boolean;
  };
}
```

This allows event consumers to receive the full question structure, not just a plain string.
  </action>
  <verify>npm run typecheck</verify>
  <done>AgentWaitingEvent includes structured question metadata</done>
</task>

</tasks>

<verification>
Before declaring plan complete:
- [ ] `npm run build` succeeds
- [ ] `npm run typecheck` passes
- [ ] Schema exports both Zod and JSON Schema formats
- [ ] ClaudeAgentManager passes --json-schema to CLI
- [ ] Question status triggers waiting_for_input with stored question
- [ ] Resume uses same session_id
</verification>

<success_criteria>

- All tasks completed
- All verification checks pass
- Agent output is validated against schema
- Question -> answer -> resume flow uses same session_id
- No breaking changes to existing tests (MockAgentManager updated separately)
</success_criteria>

<output>
After completion, create `.planning/phases/08.1-agent-output-schema/08.1-01-SUMMARY.md`
</output>
