# Gate Failure Targeted Repair Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** When the fast gate fails in the builder repair retry loop, extract universal structural diagnostics from the gate log (file refs, error lines, summary lines) and render them into a targeted repair prompt so the builder knows exactly what to fix.

**Architecture:** New pure function `extractGateLog` in `supervisor/gate-log-extractor.ts` applies platform-agnostic regex patterns to raw log content. `BuildWaveExecutor` calls it after each gate FAIL in the repair loop, refreshing context each iteration. `ReactionsService.buildRepairPrompt` renders structured output when `failureAnalysis` is present on `GateRepairContext`.

**Tech Stack:** TypeScript, Vitest, Node.js `fs/promises` (already used in `build-wave-executor.ts`)

---

## Background: How the current repair loop works

In `apps/control-plane/src/supervisor/build-wave-executor.ts`, the `run()` method:

1. Runs each BUILDING feature through the worker (applies patches)
2. Runs `GATES_RUN` in `fast` mode
3. If gate FAILs AND `reactionsService.shouldRetry()` is true, enters a retry loop:
   - Calls `reactionsService.buildRepairPrompt(ctx)` — currently returns mostly empty content
   - Runs worker again with that repair prompt
   - Runs gate again
   - Repeats up to `max_retries` (default: 2)

**The bug:** `ctx.logs` is always `''` on a normal gate FAIL (only set when a JS exception is thrown). `ctx.evidenceSummary` is just a file path string, never read. The builder gets zero useful diagnostics.

**The fix:** After each gate FAIL, re-fetch context (which has `latest_evidence.step_results[].log_path`), read the log file, extract structural signals, and pass them into `GateRepairContext.failureAnalysis`.

---

## Task 1: Create `GateLogExtractor` — pure extraction function

**Files:**

- Create: `apps/control-plane/src/supervisor/gate-log-extractor.ts`
- Create: `apps/control-plane/test/gate-log-extractor.spec.ts`

### Step 1: Write the failing tests

Create `apps/control-plane/test/gate-log-extractor.spec.ts`:

```typescript
import { describe, expect, it } from 'vitest';
import { extractGateLog } from '../src/supervisor/gate-log-extractor.js';

describe('extractGateLog', () => {
  it('GIVEN_log_with_file_line_col_message_WHEN_extracted_THEN_affectedFiles_populated', () => {
    const result = extractGateLog({
      stepName: 'typecheck',
      cmd: ['npm', 'run', 'typecheck'],
      exitCode: 1,
      logContent:
        'src/foo.ts:10:5: error TS2345: Argument of type string\nsrc/bar.ts:22:1: error TS2304: Cannot find name',
    });
    expect(result.affectedFiles).toHaveLength(2);
    expect(result.affectedFiles[0]).toMatchObject({ path: 'src/foo.ts', line: 10, col: 5 });
    expect(result.affectedFiles[1]).toMatchObject({ path: 'src/bar.ts', line: 22, col: 1 });
  });

  it('GIVEN_log_with_file_line_no_col_WHEN_extracted_THEN_col_is_null', () => {
    const result = extractGateLog({
      stepName: 'lint',
      cmd: ['npm', 'run', 'lint'],
      exitCode: 1,
      logContent: 'src/baz.ts:5: error  no-unused-vars',
    });
    expect(result.affectedFiles[0]).toMatchObject({ path: 'src/baz.ts', line: 5, col: null });
  });

  it('GIVEN_log_with_error_lines_WHEN_extracted_THEN_errorLines_populated', () => {
    const result = extractGateLog({
      stepName: 'typecheck',
      cmd: ['tsc', '--noEmit'],
      exitCode: 1,
      logContent: 'Starting compilation\nerror TS2345: bad type\nCompilation failed',
    });
    expect(result.errorLines.some((l) => l.includes('TS2345'))).toBe(true);
  });

  it('GIVEN_log_with_summary_line_WHEN_extracted_THEN_errorCount_parsed', () => {
    const result = extractGateLog({
      stepName: 'lint',
      cmd: ['eslint', '.'],
      exitCode: 1,
      logContent: 'src/foo.ts:1:1 error no-unused-vars\n✖ 3 errors found',
    });
    expect(result.errorCount).toBe(3);
    expect(result.summaryLines.some((l) => l.includes('3 errors'))).toBe(true);
  });

  it('GIVEN_long_log_WHEN_extracted_THEN_logTail_is_last_1200_chars', () => {
    const longLog = 'x'.repeat(2000);
    const result = extractGateLog({
      stepName: 'test',
      cmd: ['npm', 'test'],
      exitCode: 1,
      logContent: longLog,
    });
    expect(result.logTail).toHaveLength(1200);
    expect(result.logTail).toBe(longLog.slice(-1200));
  });

  it('GIVEN_log_shorter_than_1200_WHEN_extracted_THEN_logTail_is_full_log', () => {
    const shortLog = 'short error line';
    const result = extractGateLog({
      stepName: 'test',
      cmd: ['npm', 'test'],
      exitCode: 1,
      logContent: shortLog,
    });
    expect(result.logTail).toBe(shortLog);
  });

  it('GIVEN_duplicate_file_refs_WHEN_extracted_THEN_affectedFiles_deduplicated', () => {
    const result = extractGateLog({
      stepName: 'lint',
      cmd: ['eslint', '.'],
      exitCode: 1,
      logContent: 'src/foo.ts:10:5: error one\nsrc/foo.ts:10:5: error two',
    });
    // same path:line:col deduped
    const refs = result.affectedFiles.filter(
      (f) => f.path === 'src/foo.ts' && f.line === 10 && f.col === 5,
    );
    expect(refs).toHaveLength(1);
  });

  it('GIVEN_empty_log_WHEN_extracted_THEN_returns_empty_extraction_with_metadata', () => {
    const result = extractGateLog({
      stepName: 'typecheck',
      cmd: ['tsc'],
      exitCode: 1,
      logContent: '',
    });
    expect(result.stepName).toBe('typecheck');
    expect(result.cmd).toEqual(['tsc']);
    expect(result.exitCode).toBe(1);
    expect(result.affectedFiles).toEqual([]);
    expect(result.errorLines).toEqual([]);
    expect(result.summaryLines).toEqual([]);
    expect(result.errorCount).toBeNull();
    expect(result.logTail).toBe('');
  });

  it('GIVEN_FAILED_keyword_in_log_WHEN_extracted_THEN_in_summaryLines', () => {
    const result = extractGateLog({
      stepName: 'test',
      cmd: ['npm', 'test'],
      exitCode: 1,
      logContent: 'Test Suites: 1 failed\nTests: FAILED\nRan all test suites',
    });
    expect(result.summaryLines.some((l) => l.includes('FAILED'))).toBe(true);
  });

  it('GIVEN_cross_symbol_error_markers_WHEN_extracted_THEN_errorLines_captured', () => {
    const result = extractGateLog({
      stepName: 'lint',
      cmd: ['eslint', '.'],
      exitCode: 1,
      logContent: '  ✗ 2 problems\n  ✘ rule violated',
    });
    expect(result.errorLines.length).toBeGreaterThan(0);
  });

  it('GIVEN_more_than_30_error_lines_WHEN_extracted_THEN_capped_at_30', () => {
    const lines = Array.from({ length: 50 }, (_, i) => `src/f.ts:${i}:1: error msg ${i}`).join(
      '\n',
    );
    const result = extractGateLog({
      stepName: 'typecheck',
      cmd: ['tsc'],
      exitCode: 1,
      logContent: lines,
    });
    expect(result.errorLines.length).toBeLessThanOrEqual(30);
  });
});
```

### Step 2: Run to verify it fails

```bash
npx vitest run apps/control-plane/test/gate-log-extractor.spec.ts
```

Expected: FAIL — `Cannot find module '../src/supervisor/gate-log-extractor.js'`

### Step 3: Implement `gate-log-extractor.ts`

Create `apps/control-plane/src/supervisor/gate-log-extractor.ts`:

```typescript
export interface FileRef {
  path: string;
  line: number | null;
  col: number | null;
  message: string | null;
}

export interface GateLogExtraction {
  stepName: string;
  cmd: string[];
  exitCode: number;
  affectedFiles: FileRef[];
  errorLines: string[];
  summaryLines: string[];
  errorCount: number | null;
  logTail: string;
}

const LOG_TAIL_MAX = 1200;
const ERROR_LINES_MAX = 30;
const SUMMARY_LINES_MAX = 10;

// Matches: path/to/file.ext:line:col: message  OR  path/to/file.ext:line: message
const FILE_REF_RE = /\b([\w./\-]+\.\w{1,10}):(\d+)(?::(\d+))?(?:[,:][ \t]*(.+))?/g;

// Lines that indicate an error/failure (case-insensitive)
const ERROR_LINE_RE = /\berror\b|FAILED|\bfailed\b|✗|✘/i;

// Lines that are summary/count lines
const SUMMARY_LINE_RE = /\d+\s+errors?|\d+\s+failures?|\bFAILED\b|\bPASS\b|\d+\s+problems?/;

// Extract error count from a summary line
const ERROR_COUNT_RE = /(\d+)\s+errors?/;

export function extractGateLog(input: {
  stepName: string;
  cmd: string[];
  exitCode: number;
  logContent: string;
}): GateLogExtraction {
  const { stepName, cmd, exitCode, logContent } = input;

  const logTail = logContent.length > LOG_TAIL_MAX ? logContent.slice(-LOG_TAIL_MAX) : logContent;

  const lines = logContent.split('\n');

  // Extract error lines (capped)
  const errorLines: string[] = [];
  for (const line of lines) {
    if (errorLines.length >= ERROR_LINES_MAX) break;
    if (ERROR_LINE_RE.test(line)) {
      errorLines.push(line);
    }
  }

  // Extract summary lines (capped)
  const summaryLines: string[] = [];
  for (const line of lines) {
    if (summaryLines.length >= SUMMARY_LINES_MAX) break;
    if (SUMMARY_LINE_RE.test(line)) {
      summaryLines.push(line);
    }
  }

  // Parse error count from first matching summary line
  let errorCount: number | null = null;
  for (const line of summaryLines) {
    const match = ERROR_COUNT_RE.exec(line);
    if (match) {
      errorCount = parseInt(match[1], 10);
      break;
    }
  }

  // Extract file references (deduplicated by path:line:col)
  const seenRefs = new Set<string>();
  const affectedFiles: FileRef[] = [];
  FILE_REF_RE.lastIndex = 0;
  let match: RegExpExecArray | null;
  while ((match = FILE_REF_RE.exec(logContent)) !== null) {
    const path = match[1];
    const line = parseInt(match[2], 10);
    const col = match[3] != null ? parseInt(match[3], 10) : null;
    const message = match[4]?.trim() ?? null;
    const key = `${path}:${line}:${col ?? ''}`;
    if (!seenRefs.has(key)) {
      seenRefs.add(key);
      affectedFiles.push({ path, line, col, message });
    }
  }

  return {
    stepName,
    cmd,
    exitCode,
    affectedFiles,
    errorLines,
    summaryLines,
    errorCount,
    logTail,
  };
}
```

### Step 4: Run tests to verify they pass

```bash
npx vitest run apps/control-plane/test/gate-log-extractor.spec.ts
```

Expected: all tests PASS.

### Step 5: Commit

```bash
git add apps/control-plane/src/supervisor/gate-log-extractor.ts \
        apps/control-plane/test/gate-log-extractor.spec.ts
git commit -m "feat: add GateLogExtractor for platform-agnostic gate log analysis"
```

---

## Task 2: Enrich `GateRepairContext` and update `buildRepairPrompt`

**Files:**

- Modify: `apps/control-plane/src/application/services/reactions-service.ts`
- Modify: `apps/control-plane/test/reactions.spec.ts`

### Step 1: Write failing tests for the new prompt rendering

Open `apps/control-plane/test/reactions.spec.ts`. After the existing tests, add a new `describe` block:

```typescript
import type { GateLogExtraction } from '../src/supervisor/gate-log-extractor.js';

// ... add inside the existing describe('ReactionsService') block:

describe('buildRepairPrompt with failureAnalysis', () => {
  it('GIVEN_failureAnalysis_present_WHEN_buildRepairPrompt_THEN_renders_step_name_and_exit_code', () => {
    const service = new ReactionsService({
      toolCaller: makeToolCaller() as never,
      policy: makeEnabledPolicy(),
    });
    const analysis: GateLogExtraction = {
      stepName: 'test_with_lint_and_typecheck',
      cmd: ['npm', 'run', 'typecheck'],
      exitCode: 1,
      affectedFiles: [{ path: 'src/foo.ts', line: 10, col: 5, message: 'error TS2345: bad type' }],
      errorLines: ['src/foo.ts:10:5: error TS2345: bad type'],
      summaryLines: ['1 error'],
      errorCount: 1,
      logTail: 'src/foo.ts:10:5: error TS2345: bad type',
    };
    const ctx: GateRepairContext = {
      featureId: 'feat_x',
      gateName: 'fast',
      exitCode: 1,
      logs: '',
      evidenceSummary: '',
      retryCount: 0,
      failureAnalysis: analysis,
    };
    const prompt = service.buildRepairPrompt(ctx);
    expect(prompt).toContain('test_with_lint_and_typecheck');
    expect(prompt).toContain('exit 1');
  });

  it('GIVEN_failureAnalysis_with_affected_files_WHEN_buildRepairPrompt_THEN_renders_file_refs', () => {
    const service = new ReactionsService({
      toolCaller: makeToolCaller() as never,
      policy: makeEnabledPolicy(),
    });
    const analysis: GateLogExtraction = {
      stepName: 'lint',
      cmd: ['npm', 'run', 'lint'],
      exitCode: 1,
      affectedFiles: [
        { path: 'src/a.ts', line: 3, col: 1, message: 'no-unused-vars' },
        { path: 'src/b.ts', line: 7, col: null, message: null },
      ],
      errorLines: [],
      summaryLines: ['2 errors'],
      errorCount: 2,
      logTail: 'tail',
    };
    const ctx: GateRepairContext = {
      featureId: 'feat_x',
      gateName: 'fast',
      exitCode: 1,
      logs: '',
      evidenceSummary: '',
      retryCount: 0,
      failureAnalysis: analysis,
    };
    const prompt = service.buildRepairPrompt(ctx);
    expect(prompt).toContain('src/a.ts:3:1');
    expect(prompt).toContain('src/b.ts:7');
    expect(prompt).toContain('2 errors');
  });

  it('GIVEN_failureAnalysis_absent_WHEN_buildRepairPrompt_THEN_falls_back_to_generic_output', () => {
    const service = new ReactionsService({
      toolCaller: makeToolCaller() as never,
      policy: makeEnabledPolicy(),
    });
    const ctx: GateRepairContext = {
      featureId: 'feat_x',
      gateName: 'fast',
      exitCode: 1,
      logs: 'some log output',
      evidenceSummary: 'evidence path',
      retryCount: 1,
    };
    const prompt = service.buildRepairPrompt(ctx);
    expect(prompt).toContain('Gate execution failed');
    expect(prompt).toContain('some log output');
  });

  it('GIVEN_failureAnalysis_with_empty_affectedFiles_WHEN_buildRepairPrompt_THEN_renders_logTail_fallback', () => {
    const service = new ReactionsService({
      toolCaller: makeToolCaller() as never,
      policy: makeEnabledPolicy(),
    });
    const analysis: GateLogExtraction = {
      stepName: 'build',
      cmd: ['make'],
      exitCode: 2,
      affectedFiles: [],
      errorLines: [],
      summaryLines: [],
      errorCount: null,
      logTail: 'make: *** [all] Error 2',
    };
    const ctx: GateRepairContext = {
      featureId: 'feat_x',
      gateName: 'fast',
      exitCode: 2,
      logs: '',
      evidenceSummary: '',
      retryCount: 0,
      failureAnalysis: analysis,
    };
    const prompt = service.buildRepairPrompt(ctx);
    expect(prompt).toContain('make: *** [all] Error 2');
  });
});
```

### Step 2: Run to verify tests fail

```bash
npx vitest run apps/control-plane/test/reactions.spec.ts
```

Expected: FAIL — type error on `failureAnalysis` property (field doesn't exist yet).

### Step 3: Add `failureAnalysis` to `GateRepairContext` and update `buildRepairPrompt`

In `apps/control-plane/src/application/services/reactions-service.ts`:

**Add import at top:**

```typescript
import type { GateLogExtraction } from '../../supervisor/gate-log-extractor.js';
```

**Add `failureAnalysis` to `GateRepairContext`:**

```typescript
export interface GateRepairContext {
  featureId: string;
  gateName: string;
  exitCode: number;
  logs: string;
  evidenceSummary: string;
  retryCount: number;
  failureHistory?: Array<{
    attempt: number;
    gate_name: string;
    exit_code: number;
    evidence_summary: string;
    logs_excerpt: string;
    failed_at: string;
    retry_delay_ms?: number;
  }>;
  failureAnalysis?: GateLogExtraction;
}
```

**Replace `buildRepairPrompt` body** with one that renders structured output when `failureAnalysis` is present:

```typescript
buildRepairPrompt(ctx: GateRepairContext): string {
  const maxRetries = this.maxRetries();
  const delayMs = this.retryDelayMs();
  const failureSummary =
    Array.isArray(ctx.failureHistory) && ctx.failureHistory.length > 0
      ? ctx.failureHistory
          .slice(-3)
          .map(
            (item) =>
              `- attempt=${item.attempt}; gate=${item.gate_name}; exit=${item.exit_code}; at=${item.failed_at}; evidence=${item.evidence_summary || 'n/a'}`,
          )
          .join('\n')
      : '(none)';

  if (!ctx.failureAnalysis) {
    return [
      'Gate execution failed. Review the error logs below and apply fixes.',
      '',
      `Gate: ${ctx.gateName}`,
      `Exit Code: ${ctx.exitCode}`,
      '',
      'Logs:',
      ctx.logs,
      '',
      'Evidence:',
      ctx.evidenceSummary,
      '',
      `Retry: ${ctx.retryCount + 1} of ${maxRetries}`,
      `Retry delay (ms): ${delayMs}`,
      '',
      'Failure history (most recent first):',
      failureSummary,
    ].join('\n');
  }

  const { stepName, exitCode, affectedFiles, errorLines, summaryLines, errorCount, logTail } =
    ctx.failureAnalysis;

  const lines: string[] = [
    `Gate \`${ctx.gateName}\` step \`${stepName}\` failed (exit ${exitCode}).`,
    '',
  ];

  if (affectedFiles.length > 0) {
    lines.push(`Affected files (${affectedFiles.length}):`);
    for (const ref of affectedFiles) {
      const loc = ref.col != null ? `${ref.path}:${ref.line}:${ref.col}` : `${ref.path}:${ref.line}`;
      const msg = ref.message ? ` — ${ref.message}` : '';
      lines.push(`  ${loc}${msg}`);
    }
    lines.push('');
  }

  if (errorCount != null || summaryLines.length > 0) {
    const countLabel = errorCount != null ? `${errorCount} total` : '';
    lines.push(`Errors${countLabel ? ` (${countLabel})` : ''}:`);
    for (const sl of summaryLines) {
      lines.push(`  ${sl}`);
    }
    lines.push('');
  }

  if (errorLines.length > 0) {
    for (const el of errorLines) {
      lines.push(`  ${el}`);
    }
    lines.push('');
  }

  lines.push('Fix all errors listed above before re-running.');
  lines.push(`Retry: ${ctx.retryCount + 1} of ${maxRetries}`);
  lines.push(`Retry delay (ms): ${delayMs}`);
  lines.push('');
  lines.push('Failure history (most recent first):');
  lines.push(failureSummary);

  if (logTail.trim().length > 0) {
    lines.push('');
    lines.push('Log tail (for reference):');
    lines.push(logTail);
  }

  return lines.join('\n');
}
```

### Step 4: Run tests

```bash
npx vitest run apps/control-plane/test/reactions.spec.ts
```

Expected: all tests PASS.

### Step 5: Commit

```bash
git add apps/control-plane/src/application/services/reactions-service.ts \
        apps/control-plane/test/reactions.spec.ts
git commit -m "feat: enrich GateRepairContext with failureAnalysis and render structured repair prompt"
```

---

## Task 3: Wire `GateLogExtractor` into `BuildWaveExecutor` repair loop

**Files:**

- Modify: `apps/control-plane/src/supervisor/build-wave-executor.ts`
- Modify: `apps/control-plane/test/worker-execution-policy.spec.ts`

### Step 1: Write failing tests

Open `apps/control-plane/test/worker-execution-policy.spec.ts`. Locate the `buildToolCaller` helper — it already mocks `TOOLS.FEATURE_GET_CONTEXT` and `TOOLS.GATES_RUN`. Add a new `describe` block for repair loop enrichment. You'll need to also mock `fs.readFile`.

Add these tests near the end of the file (before closing braces of the top-level describe block):

```typescript
describe('BuildWaveExecutor repair loop log extraction', () => {
  it('GIVEN_gate_fails_and_log_exists_WHEN_repair_runs_THEN_context_is_refreshed_each_iteration', async () => {
    // Track how many times FEATURE_GET_CONTEXT is called
    let contextCallCount = 0;
    const toolCaller = {
      callTool: vi.fn(async (_role: string, toolName: string) => {
        if (toolName === TOOLS.FEATURE_STATE_GET) {
          return {
            ok: true,
            data: { front_matter: { status: STATUS.BUILDING, gate_retry_count: 0, version: 1 } },
          };
        }
        if (toolName === TOOLS.FEATURE_GET_CONTEXT) {
          contextCallCount += 1;
          return {
            ok: true,
            data: {
              state: { front_matter: { status: STATUS.BUILDING, gates: { fast: 'fail' } } },
              latest_evidence: {
                overall: 'fail',
                mode: 'fast',
                coverage_status: 'unknown',
                step_results: [{ name: 'typecheck', exit_code: 1, log_path: '/tmp/test.log' }],
              },
            },
          };
        }
        if (toolName === TOOLS.GATES_RUN) {
          return { ok: true, data: { overall: 'fail', evidence_path: 'evidence.json' } };
        }
        if (toolName === TOOLS.FEATURE_STATE_PATCH) {
          return { ok: true, data: {} };
        }
        if (toolName === TOOLS.FEATURE_LOG_APPEND) {
          return { ok: true, data: {} };
        }
        return { ok: true, data: {} };
      }),
    };
    vi.spyOn(fs, 'readFile').mockResolvedValue(
      'src/foo.ts:1:1: error TS2345: bad type\n1 error' as never,
    );

    const workerDecisionRunner = {
      execute: vi.fn(async () => ({
        planSubmission: false,
        patchApplied: true,
        noteLogged: false,
        requestHandled: false,
        invalidOutput: false,
        noProgress: false,
        outputTypes: ['PATCH'],
        rawOutputs: [],
        priorityOrder: [],
        toolResults: [],
      })),
    };
    const reactionsService = {
      shouldRetry: vi.fn().mockReturnValueOnce(true).mockReturnValue(false),
      shouldEscalate: vi.fn().mockReturnValue(false),
      retryDelayMs: vi.fn().mockReturnValue(0),
      waitBeforeRetry: vi.fn().mockResolvedValue(undefined),
      buildRepairPrompt: vi.fn().mockReturnValue('repair prompt'),
      recordRetry: vi.fn().mockResolvedValue(undefined),
    };

    const executor = new BuildWaveExecutor({
      toolCaller: toolCaller as any,
      workerDecisionRunner: workerDecisionRunner as any,
      reactionsService: reactionsService as any,
      providerMode: 'live',
      repoRoot: '/repo',
    });

    await executor.run(['feature_a'], 1);

    // context should be fetched: once pre-work + once after initial gate fail + once per retry iteration
    expect(contextCallCount).toBeGreaterThanOrEqual(2);
  });

  it('GIVEN_gate_fails_and_log_readable_WHEN_repair_runs_THEN_buildRepairPrompt_receives_failureAnalysis', async () => {
    const toolCaller = {
      callTool: vi.fn(async (_role: string, toolName: string) => {
        if (toolName === TOOLS.FEATURE_STATE_GET) {
          return {
            ok: true,
            data: { front_matter: { status: STATUS.BUILDING, gate_retry_count: 0, version: 1 } },
          };
        }
        if (toolName === TOOLS.FEATURE_GET_CONTEXT) {
          return {
            ok: true,
            data: {
              state: { front_matter: { status: STATUS.BUILDING, gates: {} } },
              latest_evidence: {
                overall: 'fail',
                mode: 'fast',
                coverage_status: 'unknown',
                step_results: [{ name: 'lint', exit_code: 1, log_path: '/tmp/lint.log' }],
              },
            },
          };
        }
        if (toolName === TOOLS.GATES_RUN) {
          return { ok: true, data: { overall: 'fail', evidence_path: 'ev.json' } };
        }
        if (toolName === TOOLS.FEATURE_STATE_PATCH) return { ok: true, data: {} };
        if (toolName === TOOLS.FEATURE_LOG_APPEND) return { ok: true, data: {} };
        return { ok: true, data: {} };
      }),
    };
    vi.spyOn(fs, 'readFile').mockResolvedValue(
      'src/a.ts:5:3: error no-unused-vars\n1 error' as never,
    );

    const workerDecisionRunner = {
      execute: vi.fn(async () => ({
        planSubmission: false,
        patchApplied: true,
        noteLogged: false,
        requestHandled: false,
        invalidOutput: false,
        noProgress: false,
        outputTypes: ['PATCH'],
        rawOutputs: [],
        priorityOrder: [],
        toolResults: [],
      })),
    };
    const capturedCtxs: unknown[] = [];
    const reactionsService = {
      shouldRetry: vi.fn().mockReturnValueOnce(true).mockReturnValue(false),
      shouldEscalate: vi.fn().mockReturnValue(false),
      retryDelayMs: vi.fn().mockReturnValue(0),
      waitBeforeRetry: vi.fn().mockResolvedValue(undefined),
      buildRepairPrompt: vi.fn((ctx: unknown) => {
        capturedCtxs.push(ctx);
        return 'repair';
      }),
      recordRetry: vi.fn().mockResolvedValue(undefined),
    };

    const executor = new BuildWaveExecutor({
      toolCaller: toolCaller as any,
      workerDecisionRunner: workerDecisionRunner as any,
      reactionsService: reactionsService as any,
      providerMode: 'live',
      repoRoot: '/repo',
    });

    await executor.run(['feature_a'], 1);

    expect(reactionsService.buildRepairPrompt).toHaveBeenCalled();
    const ctx = capturedCtxs[0] as Record<string, unknown>;
    expect(ctx).toHaveProperty('failureAnalysis');
    const analysis = ctx.failureAnalysis as Record<string, unknown>;
    expect(analysis.stepName).toBe('lint');
    expect(analysis.exitCode).toBe(1);
    expect(Array.isArray(analysis.affectedFiles)).toBe(true);
  });

  it('GIVEN_log_file_unreadable_WHEN_repair_runs_THEN_failureAnalysis_absent_and_no_crash', async () => {
    const toolCaller = {
      callTool: vi.fn(async (_role: string, toolName: string) => {
        if (toolName === TOOLS.FEATURE_STATE_GET) {
          return {
            ok: true,
            data: { front_matter: { status: STATUS.BUILDING, gate_retry_count: 0, version: 1 } },
          };
        }
        if (toolName === TOOLS.FEATURE_GET_CONTEXT) {
          return {
            ok: true,
            data: {
              state: { front_matter: { status: STATUS.BUILDING, gates: {} } },
              latest_evidence: {
                overall: 'fail',
                mode: 'fast',
                coverage_status: 'unknown',
                step_results: [{ name: 'lint', exit_code: 1, log_path: '/tmp/missing.log' }],
              },
            },
          };
        }
        if (toolName === TOOLS.GATES_RUN) {
          return { ok: true, data: { overall: 'fail', evidence_path: 'ev.json' } };
        }
        if (toolName === TOOLS.FEATURE_STATE_PATCH) return { ok: true, data: {} };
        if (toolName === TOOLS.FEATURE_LOG_APPEND) return { ok: true, data: {} };
        return { ok: true, data: {} };
      }),
    };
    vi.spyOn(fs, 'readFile').mockRejectedValue(new Error('ENOENT'));

    const workerDecisionRunner = {
      execute: vi.fn(async () => ({
        planSubmission: false,
        patchApplied: true,
        noteLogged: false,
        requestHandled: false,
        invalidOutput: false,
        noProgress: false,
        outputTypes: ['PATCH'],
        rawOutputs: [],
        priorityOrder: [],
        toolResults: [],
      })),
    };
    const capturedCtxs: unknown[] = [];
    const reactionsService = {
      shouldRetry: vi.fn().mockReturnValueOnce(true).mockReturnValue(false),
      shouldEscalate: vi.fn().mockReturnValue(false),
      retryDelayMs: vi.fn().mockReturnValue(0),
      waitBeforeRetry: vi.fn().mockResolvedValue(undefined),
      buildRepairPrompt: vi.fn((ctx: unknown) => {
        capturedCtxs.push(ctx);
        return 'repair';
      }),
      recordRetry: vi.fn().mockResolvedValue(undefined),
    };

    const executor = new BuildWaveExecutor({
      toolCaller: toolCaller as any,
      workerDecisionRunner: workerDecisionRunner as any,
      reactionsService: reactionsService as any,
      providerMode: 'live',
      repoRoot: '/repo',
    });

    // should not throw
    await expect(executor.run(['feature_a'], 1)).resolves.not.toThrow();
    const ctx = capturedCtxs[0] as Record<string, unknown>;
    // failureAnalysis should be absent when log read fails
    expect(ctx.failureAnalysis).toBeUndefined();
  });
});
```

### Step 2: Run to verify tests fail

```bash
npx vitest run apps/control-plane/test/worker-execution-policy.spec.ts
```

Expected: FAIL — `context not refreshed` / `failureAnalysis` not present on context.

### Step 3: Implement the changes in `build-wave-executor.ts`

**Add import** at top of file (with other imports):

```typescript
import { extractGateLog } from './gate-log-extractor.js';
```

**Add private helper** at the bottom of the class (before the closing `}`):

```typescript
private async extractFailureAnalysis(
  contextData: Record<string, unknown>,
  exitCode: number,
): Promise<import('./gate-log-extractor.js').GateLogExtraction | null> {
  try {
    const latestEvidence = this.asRecord(contextData.latest_evidence);
    const stepResultsRaw = Array.isArray(latestEvidence?.step_results)
      ? latestEvidence.step_results
      : [];
    const stepResults = stepResultsRaw
      .map((item: unknown) => asRecord(item))
      .filter((item): item is Record<string, unknown> => Boolean(item));

    const failingStep =
      stepResults.find((item) => (asNumber(item.exit_code) ?? 0) !== 0) ??
      stepResults[0] ??
      null;
    if (!failingStep) return null;

    const stepName = asString(failingStep.name) ?? 'unknown';
    const stepCmd: string[] = Array.isArray(failingStep.cmd)
      ? (failingStep.cmd as unknown[]).map((c) => String(c))
      : [];
    const rawLogPath = asString(failingStep.log_path);
    const logPath = rawLogPath
      ? path.isAbsolute(rawLogPath)
        ? rawLogPath
        : this.repoRoot
          ? path.join(this.repoRoot, rawLogPath)
          : rawLogPath
      : null;

    if (!logPath) return null;

    const logContent = await fs.readFile(logPath, 'utf8');
    return extractGateLog({ stepName, cmd: stepCmd, exitCode, logContent });
  } catch {
    return null;
  }
}
```

**Update the repair loop** — locate the section starting at `if (this.reactionsService && gateOverall === GATE_RESULT.FAIL && gateRetryEligible)`. Within it:

1. **Refresh context before entering the loop** — the `context` variable is already fetched before this block. After the initial gate FAIL, add a context refresh and extraction:

Replace the block that reads:

```typescript
if (this.reactionsService && gateOverall === GATE_RESULT.FAIL && gateRetryEligible) {
  const context = await this.toolCaller.callTool('builder', TOOLS.FEATURE_GET_CONTEXT, {
    feature_id: featureId,
  });
  let retryCount = initialRetryCount;
```

With:

```typescript
if (this.reactionsService && gateOverall === GATE_RESULT.FAIL && gateRetryEligible) {
  let repairContext = await this.toolCaller.callTool('builder', TOOLS.FEATURE_GET_CONTEXT, {
    feature_id: featureId,
  });
  let retryCount = initialRetryCount;
```

2. **Add extraction before `buildRepairPrompt`** — locate where `repairPrompt` is built inside the while loop. Replace:

```typescript
const repairPrompt = this.reactionsService.buildRepairPrompt(ctx);
```

With:

```typescript
const failureAnalysis = await this.extractFailureAnalysis(
  repairContext.data as Record<string, unknown>,
  gateExitCode,
);
const ctx: GateRepairContext = {
  featureId,
  gateName: 'fast',
  exitCode: gateExitCode,
  logs: gateLogs,
  evidenceSummary: gateEvidencePath,
  retryCount,
  failureHistory,
  ...(failureAnalysis ? { failureAnalysis } : {}),
};
const repairPrompt = this.reactionsService.buildRepairPrompt(ctx);
```

> Note: the existing code already builds `ctx` before calling `buildRepairPrompt`. You are adding `failureAnalysis` into that construction. Check the exact variable names in the file and match them.

3. **Refresh context before each repair worker call** — locate the line that passes `context.data` as `contextBundle` to `workerDecisionRunner.execute`. Replace `context.data` with `repairContext.data`, and before the `execute` call add:

```typescript
repairContext = await this.toolCaller.callTool('builder', TOOLS.FEATURE_GET_CONTEXT, {
  feature_id: featureId,
});
```

### Step 4: Run tests

```bash
npx vitest run apps/control-plane/test/worker-execution-policy.spec.ts
```

Expected: all new tests PASS. Run the full file to make sure existing tests still pass too.

### Step 5: Run the full test suite

```bash
npx vitest run apps/control-plane/test/reactions.spec.ts \
              apps/control-plane/test/gate-log-extractor.spec.ts \
              apps/control-plane/test/worker-execution-policy.spec.ts
```

Expected: all PASS.

### Step 6: Commit

```bash
git add apps/control-plane/src/supervisor/build-wave-executor.ts \
        apps/control-plane/test/worker-execution-policy.spec.ts
git commit -m "feat: wire GateLogExtractor into repair loop with context refresh"
```

---

## Task 4: Final verification and progress log

### Step 1: Run the full test suite

```bash
npm run test
```

Expected: all test files pass, coverage thresholds met (90% lines/branches/functions/statements).

### Step 2: Run lint and typecheck

```bash
npm run lint
npm run typecheck
```

Expected: clean (zero warnings, zero errors).

### Step 3: Append to `spec-files/progress.md`

Add a new numbered entry (increment from the last entry) in the **Completed Tasks** section:

```
✅ **Entry N — Gate Failure Targeted Repair Strategy**
   - **Goal:** Give the builder actionable diagnostics when the fast gate fails in the repair retry loop, replacing an always-empty repair prompt with structured file:line error extraction.
   - **Changes made:**
     - `apps/control-plane/src/supervisor/gate-log-extractor.ts` — new pure `extractGateLog` function; universal regex extraction of file refs, error lines, summary lines, error count, log tail.
     - `apps/control-plane/src/application/services/reactions-service.ts` — added `failureAnalysis?: GateLogExtraction` to `GateRepairContext`; updated `buildRepairPrompt` to render structured file:line output when present, falling back to generic output when absent.
     - `apps/control-plane/src/supervisor/build-wave-executor.ts` — repair loop now re-fetches context after each gate FAIL, calls `extractFailureAnalysis` to read step log and extract diagnostics, passes `failureAnalysis` into `GateRepairContext`.
     - `apps/control-plane/test/gate-log-extractor.spec.ts` — 10 unit tests covering extraction patterns, deduplication, capping, empty input.
     - `apps/control-plane/test/reactions.spec.ts` — 4 tests for structured prompt rendering with and without `failureAnalysis`.
     - `apps/control-plane/test/worker-execution-policy.spec.ts` — 3 tests for context refresh and log extraction wiring in repair loop.
   - **Result:** `npm test` ✅. `npm run lint` ✅. `npm run typecheck` ✅.
```

### Step 4: Commit

```bash
git add spec-files/progress.md
git commit -m "chore: update progress log for gate failure targeted repair"
```
