# Drop Zone File Loading & Provider-Specific Agent Optimization — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add drag-and-drop file/folder loading to the Launch Workspace UI and a prompt overlay system that lets the orchestrator leverage provider-specific capabilities (Claude's planner, Codex's task model) during planning and building phases.

**Architecture:** Two independent feature tracks in one plan. Track A (Tasks 1-2) adds HTML5 drag-and-drop to the Next.js Launch Workspace, feeding into the existing draft pipeline. Track B (Tasks 3-8) adds a prompt overlay resolver to the CLI worker provider, an artifact stager service for builder worktrees, overlay prompt files for Claude and Codex, and schema/config updates. Both tracks can be implemented in any order.

**Tech Stack:** Next.js 16 (React 18), TypeScript, Vitest, AJV JSON Schema validation, Node.js fs

---

## File Map

### New Files

| File                                                              | Responsibility                                                                        |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `packages/web-dashboard/src/lib/drop-zone-utils.ts`               | Pure async utility: recursively collect `.md` files from `DataTransferItem[]` entries |
| `apps/control-plane/src/supervisor/artifact-stager.ts`            | Copies spec/plan artifacts into `.aop/context/` in builder worktree                   |
| `apps/control-plane/test/artifact-stager.spec.ts`                 | Unit tests for artifact stager                                                        |
| `apps/control-plane/test/prompt-overlay.spec.ts`                  | Unit tests for prompt overlay resolution                                              |
| `config/agentic/orchestrator/prompts/overrides/planner.claude.md` | Claude planner overlay: use /plan mode, extended thinking                             |
| `config/agentic/orchestrator/prompts/overrides/planner.codex.md`  | Codex planner overlay: structured task decomposition                                  |
| `config/agentic/orchestrator/prompts/overrides/builder.claude.md` | Claude builder overlay: read staged artifacts, task checklist workflow                |
| `config/agentic/orchestrator/prompts/overrides/builder.codex.md`  | Codex builder overlay: read staged artifacts, apply-patch model                       |

### Modified Files

| File                                                       | Change                                                                                                                                                                                           |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `packages/web-dashboard/src/app/launch/page.tsx`           | Add drag-and-drop handlers, drop zone overlay, `dragActive` state                                                                                                                                |
| `packages/web-dashboard/src/styles/dashboard.module.css`   | Add `.dropZoneOverlay` and `.dropZoneActive` CSS classes                                                                                                                                         |
| `apps/control-plane/src/providers/cli-worker-provider.ts`  | Add `resolvePromptOverlay()` function, call it in `runWorker()` before `buildWorkerPrompt()`. Update builder file guidance in `buildInteractiveWorkerPrompt()` to include `.aop/context/` paths. |
| `apps/control-plane/src/supervisor/build-wave-executor.ts` | Import and call `ArtifactStager.stage()` before builder dispatch                                                                                                                                 |
| `config/agentic/orchestrator/agents.yaml`                  | Add `prompt_overlay_dir` field under `runtime`                                                                                                                                                   |
| `agentic/orchestrator/schemas/agents.schema.json`          | Add `prompt_overlay_dir` property to `runtime` object                                                                                                                                            |

---

### Task 1: Drop Zone Utility — `collectMarkdownFiles`

**Files:**

- Create: `packages/web-dashboard/src/lib/drop-zone-utils.ts`

- [ ] **Step 1: Create the utility file with types and the recursive collector**

```typescript
// packages/web-dashboard/src/lib/drop-zone-utils.ts

/**
 * Recursively collects .md files from a list of DataTransferItem entries.
 * Handles both direct file drops and folder drops via webkitGetAsEntry().
 */

function isMarkdownFile(name: string): boolean {
  return name.endsWith('.md');
}

function readEntriesPromise(reader: FileSystemDirectoryReader): Promise<FileSystemEntry[]> {
  return new Promise((resolve, reject) => {
    reader.readEntries(resolve, reject);
  });
}

function fileEntryToFile(entry: FileSystemFileEntry): Promise<File> {
  return new Promise((resolve, reject) => {
    entry.file(resolve, reject);
  });
}

async function walkEntry(entry: FileSystemEntry, files: File[]): Promise<void> {
  if (entry.isFile) {
    if (isMarkdownFile(entry.name)) {
      const file = await fileEntryToFile(entry as FileSystemFileEntry);
      files.push(file);
    }
    return;
  }
  if (entry.isDirectory) {
    const reader = (entry as FileSystemDirectoryEntry).createReader();
    let batch = await readEntriesPromise(reader);
    while (batch.length > 0) {
      for (const child of batch) {
        await walkEntry(child, files);
      }
      batch = await readEntriesPromise(reader);
    }
  }
}

export interface DropResult {
  markdownFiles: File[];
  skippedNonMarkdownCount: number;
}

export async function collectMarkdownFromDrop(items: DataTransferItemList): Promise<DropResult> {
  const files: File[] = [];
  let skippedNonMarkdownCount = 0;

  const entries: Array<{ entry: FileSystemEntry | null; item: DataTransferItem }> = [];
  for (let i = 0; i < items.length; i++) {
    const item = items[i];
    if (item.kind !== 'file') {
      continue;
    }
    const entry = typeof item.webkitGetAsEntry === 'function' ? item.webkitGetAsEntry() : null;
    entries.push({ entry, item });
  }

  for (const { entry, item } of entries) {
    if (entry?.isDirectory) {
      await walkEntry(entry, files);
    } else if (entry?.isFile) {
      if (isMarkdownFile(entry.name)) {
        const file = await fileEntryToFile(entry as FileSystemFileEntry);
        files.push(file);
      } else {
        skippedNonMarkdownCount++;
      }
    } else {
      // Fallback: no webkitGetAsEntry support — use getAsFile()
      const file = item.getAsFile();
      if (file) {
        if (isMarkdownFile(file.name)) {
          files.push(file);
        } else {
          skippedNonMarkdownCount++;
        }
      }
    }
  }

  return { markdownFiles: files, skippedNonMarkdownCount };
}

export function supportsDirectoryDrop(): boolean {
  if (typeof DataTransferItem === 'undefined') {
    return false;
  }
  return typeof DataTransferItem.prototype.webkitGetAsEntry === 'function';
}
```

- [ ] **Step 2: Verify the file compiles**

Run: `npx tsc --noEmit --project packages/web-dashboard/tsconfig.json 2>&1 | head -20`
Expected: Clean or only pre-existing warnings (the `FileSystemEntry` types come from `lib.dom.d.ts`).

Note: If `FileSystemEntry` types are missing, check `packages/web-dashboard/tsconfig.json` includes `"lib": ["dom"]`. If so, these types are available. If not, add type declarations inline — but this is extremely unlikely with Next.js 16.

- [ ] **Step 3: Commit**

```bash
git add packages/web-dashboard/src/lib/drop-zone-utils.ts
git commit -m "feat: add drop zone utility for recursive markdown file collection"
```

---

### Task 2: Drop Zone UI — Launch Workspace Integration

**Files:**

- Modify: `packages/web-dashboard/src/app/launch/page.tsx`
- Modify: `packages/web-dashboard/src/styles/dashboard.module.css`

- [ ] **Step 1: Add CSS classes for the drop zone overlay**

Append to the end of `packages/web-dashboard/src/styles/dashboard.module.css` (before the final closing brace if there is one, or at the very end):

```css
.dropZoneOverlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: rgba(45, 106, 79, 0.1);
  border: 3px dashed var(--accent-green, #2d6a4f);
  border-radius: 1rem;
  margin: 0.75rem;
  pointer-events: none;
  backdrop-filter: blur(2px);
}

.dropZoneOverlay h2 {
  font-size: 1.35rem;
  font-weight: 600;
  color: var(--accent-green, #4ade80);
  margin: 0;
}

.dropZoneOverlay p {
  font-size: 0.9rem;
  color: var(--text-muted, #888);
  margin: 0.5rem 0 0;
}
```

- [ ] **Step 2: Add drag-and-drop state and handlers to `LaunchPageContent`**

In `packages/web-dashboard/src/app/launch/page.tsx`, add the import at the top with the other imports:

```typescript
import { collectMarkdownFromDrop } from '@/lib/drop-zone-utils.js';
```

Inside the `LaunchPageContent` function, after the existing `useState` declarations (around line 79), add:

```typescript
const [dragActive, setDragActive] = useState(false);
const dragCounterRef = React.useRef(0);

const handleDragEnter = (event: React.DragEvent) => {
  event.preventDefault();
  event.stopPropagation();
  dragCounterRef.current += 1;
  if (dragCounterRef.current === 1) {
    setDragActive(true);
  }
};

const handleDragLeave = (event: React.DragEvent) => {
  event.preventDefault();
  event.stopPropagation();
  dragCounterRef.current -= 1;
  if (dragCounterRef.current <= 0) {
    dragCounterRef.current = 0;
    setDragActive(false);
  }
};

const handleDragOver = (event: React.DragEvent) => {
  event.preventDefault();
  event.stopPropagation();
};

const handleDrop = async (event: React.DragEvent) => {
  event.preventDefault();
  event.stopPropagation();
  dragCounterRef.current = 0;
  setDragActive(false);

  if (!event.dataTransfer?.items) {
    return;
  }

  const result = await collectMarkdownFromDrop(event.dataTransfer.items);
  if (result.markdownFiles.length === 0 && result.skippedNonMarkdownCount > 0) {
    setStatusMessage(
      `Skipped ${result.skippedNonMarkdownCount} non-markdown file${result.skippedNonMarkdownCount === 1 ? '' : 's'}. Only .md files are accepted.`,
    );
    return;
  }
  if (result.markdownFiles.length === 0) {
    return;
  }

  const loadedDrafts = await Promise.all(
    result.markdownFiles.map(async (file) => createDraft(file.name, await file.text(), 'upload')),
  );
  setDrafts((current) => [...current, ...loadedDrafts]);

  const parts: string[] = [
    `Added ${loadedDrafts.length} draft${loadedDrafts.length === 1 ? '' : 's'} from drop`,
  ];
  if (result.skippedNonMarkdownCount > 0) {
    parts.push(
      `(skipped ${result.skippedNonMarkdownCount} non-markdown file${result.skippedNonMarkdownCount === 1 ? '' : 's'})`,
    );
  }
  setStatusMessage(parts.join(' '));
};
```

- [ ] **Step 3: Attach drag event handlers and add overlay to the JSX**

In the same file, modify the root `<div>` in the return statement. Change:

```tsx
    <div className={styles.dashboardRoot}>
```

to:

```tsx
    <div
      className={styles.dashboardRoot}
      onDragEnter={handleDragEnter}
      onDragLeave={handleDragLeave}
      onDragOver={handleDragOver}
      onDrop={(event) => { void handleDrop(event); }}
    >
      {dragActive ? (
        <div className={styles.dropZoneOverlay}>
          <h2>Drop .md files or folders</h2>
          <p>Folders will be scanned for .md files</p>
        </div>
      ) : null}
```

- [ ] **Step 4: Verify the page compiles**

Run: `npx tsc --noEmit --project packages/web-dashboard/tsconfig.json 2>&1 | head -20`
Expected: Clean compilation.

- [ ] **Step 5: Commit**

```bash
git add packages/web-dashboard/src/app/launch/page.tsx packages/web-dashboard/src/styles/dashboard.module.css
git commit -m "feat: add drag-and-drop file/folder loading to Launch Workspace"
```

---

### Task 3: Agents Schema — Add `prompt_overlay_dir`

**Files:**

- Modify: `agentic/orchestrator/schemas/agents.schema.json`
- Modify: `config/agentic/orchestrator/agents.yaml`

- [ ] **Step 1: Add `prompt_overlay_dir` to the schema**

In `agentic/orchestrator/schemas/agents.schema.json`, inside the `runtime.properties` object, add the new field. Find the `"provider_cli_config"` property (around line 195) and add the new property **before** it:

```json
        "prompt_overlay_dir": {
          "type": "string",
          "description": "Directory (relative to repo root) containing provider-specific prompt overlay files. Files are named <role>.<provider>.md (e.g. builder.claude.md). When present, the overlay is appended to the base role prompt. Defaults to config/agentic/orchestrator/prompts/overrides.",
          "default": "config/agentic/orchestrator/prompts/overrides"
        },
```

- [ ] **Step 2: Add `prompt_overlay_dir` to agents.yaml**

In `config/agentic/orchestrator/agents.yaml`, inside the `runtime:` block, add after the `execution_mode: interactive` line:

```yaml
prompt_overlay_dir: config/agentic/orchestrator/prompts/overrides
```

- [ ] **Step 3: Validate the schema**

Run: `npm run validate:mcp-contracts 2>&1 | tail -5`
Expected: Clean validation (no errors).

Run: `npm run build 2>&1 | tail -10`
Expected: Clean build.

- [ ] **Step 4: Commit**

```bash
git add agentic/orchestrator/schemas/agents.schema.json config/agentic/orchestrator/agents.yaml
git commit -m "feat: add prompt_overlay_dir config for provider-specific prompt overlays"
```

---

### Task 4: Prompt Overlay Resolver — Unit Tests

**Files:**

- Create: `apps/control-plane/test/prompt-overlay.spec.ts`

- [ ] **Step 1: Write the test file**

The overlay resolver will be a pure function exported from `cli-worker-provider.ts`. We test it in isolation by calling it directly. Since `cli-worker-provider.ts` uses the filesystem to read overlay files, we'll use a temp directory approach.

```typescript
// apps/control-plane/test/prompt-overlay.spec.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import fs from 'node:fs/promises';
import path from 'node:path';
import os from 'node:os';
import { resolvePromptOverlay } from '../src/providers/cli-worker-provider.js';

describe('resolvePromptOverlay', () => {
  let tmpDir: string;

  beforeEach(async () => {
    tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'overlay-test-'));
  });

  afterEach(async () => {
    await fs.rm(tmpDir, { recursive: true, force: true });
  });

  it('GIVEN_overlay_file_exists_WHEN_resolved_THEN_returns_overlay_content', async () => {
    const overlayContent = '## Claude Builder Instructions\n\nUse /plan mode.';
    await fs.mkdir(path.join(tmpDir, 'overrides'), { recursive: true });
    await fs.writeFile(path.join(tmpDir, 'overrides', 'builder.claude.md'), overlayContent);

    const result = await resolvePromptOverlay(path.join(tmpDir, 'overrides'), 'builder', 'claude');

    expect(result).toBe(overlayContent);
  });

  it('GIVEN_no_overlay_file_WHEN_resolved_THEN_returns_null', async () => {
    await fs.mkdir(path.join(tmpDir, 'overrides'), { recursive: true });

    const result = await resolvePromptOverlay(path.join(tmpDir, 'overrides'), 'builder', 'codex');

    expect(result).toBeNull();
  });

  it('GIVEN_no_overlay_directory_WHEN_resolved_THEN_returns_null', async () => {
    const result = await resolvePromptOverlay(
      path.join(tmpDir, 'nonexistent'),
      'builder',
      'claude',
    );

    expect(result).toBeNull();
  });

  it('GIVEN_planner_role_with_claude_WHEN_resolved_THEN_reads_planner_claude_md', async () => {
    const overlayContent = '## Claude Planner Instructions';
    await fs.mkdir(path.join(tmpDir, 'overrides'), { recursive: true });
    await fs.writeFile(path.join(tmpDir, 'overrides', 'planner.claude.md'), overlayContent);

    const result = await resolvePromptOverlay(path.join(tmpDir, 'overrides'), 'planner', 'claude');

    expect(result).toBe(overlayContent);
  });

  it('GIVEN_empty_overlay_file_WHEN_resolved_THEN_returns_null', async () => {
    await fs.mkdir(path.join(tmpDir, 'overrides'), { recursive: true });
    await fs.writeFile(path.join(tmpDir, 'overrides', 'builder.claude.md'), '   \n  ');

    const result = await resolvePromptOverlay(path.join(tmpDir, 'overrides'), 'builder', 'claude');

    expect(result).toBeNull();
  });
});
```

- [ ] **Step 2: Run the tests — they should fail because `resolvePromptOverlay` doesn't exist yet**

Run: `npx vitest run apps/control-plane/test/prompt-overlay.spec.ts 2>&1 | tail -15`
Expected: FAIL — cannot find `resolvePromptOverlay` export.

- [ ] **Step 3: Commit the failing tests**

```bash
git add apps/control-plane/test/prompt-overlay.spec.ts
git commit -m "test: add failing tests for prompt overlay resolver"
```

---

### Task 5: Prompt Overlay Resolver — Implementation

**Files:**

- Modify: `apps/control-plane/src/providers/cli-worker-provider.ts`

- [ ] **Step 1: Add the `resolvePromptOverlay` function**

In `apps/control-plane/src/providers/cli-worker-provider.ts`, add the following exported function after the existing `buildWorkerPrompt` function (after line 291):

```typescript
export async function resolvePromptOverlay(
  overlayDir: string,
  role: string,
  provider: string,
): Promise<string | null> {
  const filename = `${role}.${provider}.md`;
  const overlayPath = path.join(overlayDir, filename);
  try {
    const content = await fs.readFile(overlayPath, 'utf8');
    return content.trim().length > 0 ? content : null;
  } catch {
    return null;
  }
}
```

- [ ] **Step 2: Integrate overlay resolution into `runWorker()`**

In the same file, in the `runWorker` method (around line 913), modify the prompt assembly. Find these lines (around lines 952-954):

```typescript
const systemPrompt = this.systemPromptsBySession.get(sessionId) ?? null;
const prompt = buildWorkerPrompt(promptPayload, systemPrompt, executionMode);
```

Replace them with:

```typescript
const baseSystemPrompt = this.systemPromptsBySession.get(sessionId) ?? null;
let systemPrompt = baseSystemPrompt;
if (baseSystemPrompt && this.overlayDir) {
  const overlay = await resolvePromptOverlay(
    this.overlayDir,
    input.role,
    effectiveSelection.provider,
  );
  if (overlay) {
    systemPrompt = `${baseSystemPrompt}\n\n---\n\n## Provider-Specific Instructions\n\n${overlay}`;
  }
}
const prompt = buildWorkerPrompt(promptPayload, systemPrompt, executionMode);
```

- [ ] **Step 3: Add the `overlayDir` property to `CliWorkerProviderOptions` and constructor**

In the `CliWorkerProviderOptions` interface (around line 25), add:

```typescript
  overlayDir?: string;
```

In the `CliWorkerProvider` class, add a private field after the other private fields (around line 765):

```typescript
  private readonly overlayDir: string | null;
```

In the constructor (around line 824), after the line `this.repoRoot = options.repoRoot ?? null;` add:

```typescript
this.overlayDir = options.overlayDir ?? null;
```

- [ ] **Step 4: Run the overlay tests**

Run: `npx vitest run apps/control-plane/test/prompt-overlay.spec.ts 2>&1 | tail -15`
Expected: All 5 tests PASS.

- [ ] **Step 5: Run the full test suite**

Run: `npm run test 2>&1 | tail -20`
Expected: All existing tests still pass.

- [ ] **Step 6: Commit**

```bash
git add apps/control-plane/src/providers/cli-worker-provider.ts
git commit -m "feat: add prompt overlay resolver for provider-specific instructions"
```

---

### Task 6: Wire `overlayDir` Through the Runtime

**Files:**

- Modify: `apps/control-plane/src/supervisor/runtime.ts`
- Modify: `apps/control-plane/src/providers/worker-provider-factory.ts`

The `overlayDir` needs to flow from `agents.yaml` config through to the `CliWorkerProvider` constructor. The worker provider is created in the `WorkerProviderFactory`.

- [ ] **Step 1: Find where `CliWorkerProviderOptions` is constructed in the factory**

Read `apps/control-plane/src/providers/worker-provider-factory.ts` to find where `CliWorkerProvider` is instantiated. The factory constructs the provider with its options. Add `overlayDir` to the options passed through.

In the `DefaultWorkerProviderFactory`, find where `CliWorkerProviderOptions` is assembled and add:

```typescript
overlayDir: this.overlayDir,
```

Add a private `overlayDir` field and accept it in the factory constructor/options.

- [ ] **Step 2: Pass `overlayDir` from runtime.ts to the factory**

In `apps/control-plane/src/supervisor/runtime.ts`, where the `WorkerProviderFactory` is constructed, resolve the overlay dir from the agents config:

```typescript
const agentsConfig = kernel.getAgentsConfig();
const overlayDir = agentsConfig.runtime?.prompt_overlay_dir
  ? path.join(kernel.getRepoRoot(), agentsConfig.runtime.prompt_overlay_dir)
  : path.join(kernel.getRepoRoot(), 'config/agentic/orchestrator/prompts/overrides');
```

Pass this `overlayDir` to the factory and then through to the provider.

- [ ] **Step 3: Run the full test suite**

Run: `npm run test 2>&1 | tail -20`
Expected: All tests pass. The overlay dir is optional so existing test mocks are unaffected.

- [ ] **Step 4: Commit**

```bash
git add apps/control-plane/src/providers/worker-provider-factory.ts apps/control-plane/src/supervisor/runtime.ts
git commit -m "feat: wire prompt overlay directory from agents config to worker provider"
```

---

### Task 7: Artifact Stager — Tests and Implementation

**Files:**

- Create: `apps/control-plane/src/supervisor/artifact-stager.ts`
- Create: `apps/control-plane/test/artifact-stager.spec.ts`

- [ ] **Step 1: Write the failing tests**

```typescript
// apps/control-plane/test/artifact-stager.spec.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import fs from 'node:fs/promises';
import path from 'node:path';
import os from 'node:os';
import { ArtifactStager } from '../src/supervisor/artifact-stager.js';

describe('ArtifactStager', () => {
  let repoRoot: string;
  let worktreePath: string;
  const featureId = 'feat-abc-123';

  beforeEach(async () => {
    repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'stager-repo-'));
    worktreePath = await fs.mkdtemp(path.join(os.tmpdir(), 'stager-wt-'));

    const featureDir = path.join(repoRoot, '.aop', 'features', featureId);
    await fs.mkdir(featureDir, { recursive: true });
    await fs.writeFile(path.join(featureDir, 'spec.md'), '# Feature Spec\n\nBuild the thing.');
    await fs.writeFile(path.join(featureDir, 'plan.md'), '# Plan\n\n1. Do step one.');
    await fs.writeFile(
      path.join(featureDir, 'plan.json'),
      JSON.stringify({ version: 1, tasks: [] }),
    );
  });

  afterEach(async () => {
    await fs.rm(repoRoot, { recursive: true, force: true });
    await fs.rm(worktreePath, { recursive: true, force: true });
  });

  it('GIVEN_all_artifacts_exist_WHEN_staged_THEN_copies_to_context_dir', async () => {
    const stager = new ArtifactStager(repoRoot);
    await stager.stage(featureId, worktreePath);

    const contextDir = path.join(worktreePath, '.aop', 'context');
    const specContent = await fs.readFile(path.join(contextDir, 'spec.md'), 'utf8');
    const planMdContent = await fs.readFile(path.join(contextDir, 'plan.md'), 'utf8');
    const planJsonContent = await fs.readFile(path.join(contextDir, 'plan.json'), 'utf8');

    expect(specContent).toBe('# Feature Spec\n\nBuild the thing.');
    expect(planMdContent).toBe('# Plan\n\n1. Do step one.');
    expect(JSON.parse(planJsonContent)).toEqual({ version: 1, tasks: [] });
  });

  it('GIVEN_qa_test_index_exists_WHEN_staged_THEN_also_copies_it', async () => {
    const featureDir = path.join(repoRoot, '.aop', 'features', featureId);
    await fs.writeFile(
      path.join(featureDir, 'qa_test_index.json'),
      JSON.stringify({ tests: ['test1'] }),
    );

    const stager = new ArtifactStager(repoRoot);
    await stager.stage(featureId, worktreePath);

    const contextDir = path.join(worktreePath, '.aop', 'context');
    const qaContent = await fs.readFile(path.join(contextDir, 'qa_test_index.json'), 'utf8');
    expect(JSON.parse(qaContent)).toEqual({ tests: ['test1'] });
  });

  it('GIVEN_qa_test_index_missing_WHEN_staged_THEN_no_error_and_other_files_copied', async () => {
    const stager = new ArtifactStager(repoRoot);
    await stager.stage(featureId, worktreePath);

    const contextDir = path.join(worktreePath, '.aop', 'context');
    await expect(fs.access(path.join(contextDir, 'qa_test_index.json'))).rejects.toThrow();
    // But other files exist
    await expect(fs.access(path.join(contextDir, 'spec.md'))).resolves.toBeUndefined();
    await expect(fs.access(path.join(contextDir, 'plan.md'))).resolves.toBeUndefined();
  });

  it('GIVEN_context_dir_already_exists_WHEN_staged_THEN_overwrites_cleanly', async () => {
    const contextDir = path.join(worktreePath, '.aop', 'context');
    await fs.mkdir(contextDir, { recursive: true });
    await fs.writeFile(path.join(contextDir, 'spec.md'), 'old content');

    const stager = new ArtifactStager(repoRoot);
    await stager.stage(featureId, worktreePath);

    const specContent = await fs.readFile(path.join(contextDir, 'spec.md'), 'utf8');
    expect(specContent).toBe('# Feature Spec\n\nBuild the thing.');
  });

  it('GIVEN_staged_WHEN_readme_checked_THEN_readme_exists_with_content', async () => {
    const stager = new ArtifactStager(repoRoot);
    await stager.stage(featureId, worktreePath);

    const contextDir = path.join(worktreePath, '.aop', 'context');
    const readme = await fs.readFile(path.join(contextDir, 'README.md'), 'utf8');
    expect(readme).toContain('spec.md');
    expect(readme).toContain('plan.md');
    expect(readme).toContain('plan.json');
  });
});
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `npx vitest run apps/control-plane/test/artifact-stager.spec.ts 2>&1 | tail -15`
Expected: FAIL — cannot import `ArtifactStager`.

- [ ] **Step 3: Implement the `ArtifactStager`**

```typescript
// apps/control-plane/src/supervisor/artifact-stager.ts
import fs from 'node:fs/promises';
import path from 'node:path';

const REQUIRED_ARTIFACTS = ['spec.md', 'plan.md', 'plan.json'] as const;
const OPTIONAL_ARTIFACTS = ['qa_test_index.json'] as const;

const CONTEXT_README = `# Staged Artifacts

These files are read-only copies staged by the orchestrator for the builder agent.

| File | Description |
|---|---|
| spec.md | Original feature specification — read this to understand the "why" |
| plan.md | Human-readable implementation plan |
| plan.json | Structured plan with tasks and acceptance criteria |
| qa_test_index.json | Test expectations (if available) |

Do not edit these files. They are copied from the canonical feature directory.
`;

export class ArtifactStager {
  private readonly repoRoot: string;

  constructor(repoRoot: string) {
    this.repoRoot = repoRoot;
  }

  async stage(featureId: string, worktreePath: string): Promise<void> {
    const featureDir = path.join(this.repoRoot, '.aop', 'features', featureId);
    const contextDir = path.join(worktreePath, '.aop', 'context');

    await fs.mkdir(contextDir, { recursive: true });

    for (const artifact of REQUIRED_ARTIFACTS) {
      const src = path.join(featureDir, artifact);
      const dst = path.join(contextDir, artifact);
      await fs.copyFile(src, dst);
    }

    for (const artifact of OPTIONAL_ARTIFACTS) {
      const src = path.join(featureDir, artifact);
      const dst = path.join(contextDir, artifact);
      try {
        await fs.copyFile(src, dst);
      } catch {
        // Optional — skip if not present
      }
    }

    await fs.writeFile(path.join(contextDir, 'README.md'), CONTEXT_README);
  }
}
```

- [ ] **Step 4: Run the tests**

Run: `npx vitest run apps/control-plane/test/artifact-stager.spec.ts 2>&1 | tail -15`
Expected: All 5 tests PASS.

- [ ] **Step 5: Commit**

```bash
git add apps/control-plane/src/supervisor/artifact-stager.ts apps/control-plane/test/artifact-stager.spec.ts
git commit -m "feat: add artifact stager to copy spec/plan into builder worktree"
```

---

### Task 8: Wire Artifact Stager into Build Wave Executor

**Files:**

- Modify: `apps/control-plane/src/supervisor/build-wave-executor.ts`
- Modify: `apps/control-plane/src/providers/cli-worker-provider.ts`

- [ ] **Step 1: Add `ArtifactStager` to `BuildWaveExecutorDependencies`**

In `apps/control-plane/src/supervisor/build-wave-executor.ts`, add the import:

```typescript
import { ArtifactStager } from './artifact-stager.js';
```

In the `BuildWaveExecutorDependencies` interface (around line 39), add:

```typescript
  artifactStager?: ArtifactStager;
```

In the constructor, store the stager as a private field:

```typescript
  private readonly artifactStager: ArtifactStager | null;
```

In the constructor body, add:

```typescript
this.artifactStager = deps.artifactStager ?? null;
```

- [ ] **Step 2: Call `stage()` before builder dispatch**

In the `run()` method, find where `this.workerDecisionRunner.execute()` is called for the builder (around line 226). Add the staging call **before** the worker execute call. Find the context fetch and add staging after it:

After the line:

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

And before the `let decision;` line, add:

```typescript
if (this.artifactStager && this.providerMode === 'live') {
  const worktreePath = context.data?.working_directory as string | undefined;
  if (worktreePath) {
    try {
      await this.artifactStager.stage(featureId, worktreePath);
    } catch {
      // Non-fatal: builder can still function without staged artifacts
    }
  }
}
```

Note: The `working_directory` may not be on the context data directly. Check where the worktree path comes from. If it's resolved in the `WorkerDecisionLoop` instead, the staging call should happen there. The implementer should verify by reading how `resolveWorktreePath` works in `worker-decision-loop.ts` and adjust the staging call location accordingly. The key contract is: staging must happen before the builder's `runWorker()` is called, and it needs access to the worktree path.

- [ ] **Step 3: Update builder file guidance in `buildInteractiveWorkerPrompt()`**

In `apps/control-plane/src/providers/cli-worker-provider.ts`, find the builder file guidance section (around line 158-167). Change the builder block from:

```typescript
    } else if (role === 'builder') {
      lines.push(
        '',
        'Implementation artifacts are on disk — use these files as the execution baseline:',
        `  ${featureDir}/state.md           — current status and front matter`,
        `  ${featureDir}/plan.md            — canonical planning baseline`,
        `  ${featureDir}/plan.json          — machine-readable plan`,
        `  ${featureDir}/evidence/          — gate run results`,
        `  ${featureDir}/decisions.md       — decision history`,
      );
```

to:

```typescript
    } else if (role === 'builder') {
      const contextDir = workingDir ? `${workingDir}/.aop/context` : `${featureDir}/.aop/context`;
      lines.push(
        '',
        'Staged artifacts (read these first for context):',
        `  ${contextDir}/spec.md             — original feature specification`,
        `  ${contextDir}/plan.md             — human-readable implementation plan`,
        `  ${contextDir}/plan.json           — structured plan with tasks and acceptance criteria`,
        `  ${contextDir}/qa_test_index.json  — test expectations (if available)`,
        '',
        'Runtime artifacts:',
        `  ${featureDir}/state.md           — current status and front matter`,
        `  ${featureDir}/evidence/          — gate run results`,
        `  ${featureDir}/decisions.md       — decision history`,
      );
```

- [ ] **Step 4: Wire the stager in runtime.ts**

In `apps/control-plane/src/supervisor/runtime.ts`, where the `BuildWaveExecutor` is constructed (around line 466), add the artifact stager:

```typescript
import { ArtifactStager } from './artifact-stager.js';
```

Before the `BuildWaveExecutor` construction, create the stager:

```typescript
const artifactStager = new ArtifactStager(this.kernel.getRepoRoot());
```

Then add it to the dependencies object:

```typescript
this.buildWaveExecutor = new BuildWaveExecutor({
  // ... existing deps ...
  artifactStager,
});
```

- [ ] **Step 5: Run full test suite**

Run: `npm run test 2>&1 | tail -20`
Expected: All tests pass.

Run: `npm run build 2>&1 | tail -10`
Expected: Clean build.

- [ ] **Step 6: Commit**

```bash
git add apps/control-plane/src/supervisor/build-wave-executor.ts apps/control-plane/src/providers/cli-worker-provider.ts apps/control-plane/src/supervisor/runtime.ts
git commit -m "feat: wire artifact stager into build wave and update builder file guidance"
```

---

### Task 9: Provider Overlay Prompt Files

**Files:**

- Create: `config/agentic/orchestrator/prompts/overrides/planner.claude.md`
- Create: `config/agentic/orchestrator/prompts/overrides/planner.codex.md`
- Create: `config/agentic/orchestrator/prompts/overrides/builder.claude.md`
- Create: `config/agentic/orchestrator/prompts/overrides/builder.codex.md`

- [ ] **Step 1: Create the overrides directory**

```bash
mkdir -p config/agentic/orchestrator/prompts/overrides
```

- [ ] **Step 2: Write `planner.claude.md`**

```markdown
## Claude-Specific Planning Instructions

You are running inside Claude Code. Leverage its native capabilities:

### Planning approach

1. **Use extended thinking** for complex architectural decisions. When analyzing the spec, take time to reason through trade-offs before committing to an approach.

2. **Read the codebase** before planning. Use your file reading and search tools to understand existing patterns, conventions, and architecture. The plan must work within the existing codebase, not against it.

3. **Create structured, actionable plans** where each task maps to a discrete, testable change. Claude Code excels at file-level precision — every task should name exact files to create or modify.

4. **Reference the spec as source of truth.** The feature specification is staged at `.aop/context/spec.md` in the working directory. Read it before beginning the planning process. Every plan task should trace back to a spec requirement.

### Plan quality

- Each plan task should produce a commit-worthy change
- Include test expectations alongside implementation tasks
- Name exact file paths — do not use placeholder paths
- Include acceptance criteria that can be mechanically verified
```

- [ ] **Step 3: Write `planner.codex.md`**

```markdown
## Codex-Specific Planning Instructions

You are running inside Codex CLI.

### Planning approach

1. **Emphasize file-level granularity.** Codex works best with explicit file lists per task. Every plan task must name the exact files to create or modify.

2. **Keep tasks small and isolated.** Each task should touch a minimal set of files and produce a self-contained, testable change.

3. **Read existing patterns first.** Before planning, examine the files adjacent to where changes will be made. Follow established conventions.

4. **Reference the spec.** The feature specification is the source of truth for what to build. Every plan task should trace to a spec requirement.

### Plan quality

- Prefer explicit file paths over directory-level descriptions
- Each task should produce a commit-worthy change
- Include test expectations alongside implementation tasks
- Include acceptance criteria that can be mechanically verified
```

- [ ] **Step 4: Write `builder.claude.md`**

```markdown
## Claude-Specific Build Instructions

You are running inside Claude Code. Leverage its native capabilities for implementation.

### Before starting work

1. **Read the staged plan and spec.** These files are in your working directory:
   - `.aop/context/spec.md` — the original feature specification (the "why")
   - `.aop/context/plan.md` — the human-readable implementation plan
   - `.aop/context/plan.json` — the structured plan with tasks and acceptance criteria

2. **Treat the plan as a task checklist.** Work through tasks sequentially. Each task in the plan corresponds to a discrete change. Complete one task fully (including tests) before moving to the next.

### While working

3. **Use Claude Code's native tools.** Edit files directly using your file editing tools. Do not emit raw patches or diffs — make the changes on disk.

4. **Commit after each task.** Each plan task should produce its own commit. This makes the work reviewable and reversible.

5. **Run tests after each change.** Verify your work passes before moving to the next task.

6. **Check acceptance criteria.** The plan's acceptance criteria are the definition of done. Before marking a task complete, verify each criterion is met.

### Quality checks

7. **Stay within planned scope.** Only modify files listed in the plan's `allowed_areas` and `files` lists. If you need to touch an unplanned file, return a `REQUEST.action=more_context` instead.

8. **Follow existing patterns.** Read adjacent code to match conventions, import styles, error handling patterns, and test structure.
```

- [ ] **Step 5: Write `builder.codex.md`**

```markdown
## Codex-Specific Build Instructions

You are running inside Codex CLI.

### Before starting work

1. **Read the staged plan and spec.** These files are in your working directory:
   - `.aop/context/spec.md` — the original feature specification (the "why")
   - `.aop/context/plan.md` — the human-readable implementation plan
   - `.aop/context/plan.json` — the structured plan with tasks and acceptance criteria

2. **Treat the plan as a task checklist.** Work through tasks sequentially. Complete one task fully before moving to the next.

### While working

3. **Edit files directly.** Make changes on disk in the working directory. Do not emit raw diffs or patches.

4. **Commit after each task.** Each plan task should produce its own commit.

5. **Stay within the plan's `allowed_areas` and `files` lists.** If you need to modify files outside the plan, return `REQUEST.action=more_context`.

6. **Check acceptance criteria.** The plan's acceptance criteria are the definition of done for each task.

### Quality checks

7. **Follow existing patterns.** Read adjacent code to match conventions, import styles, and test structure.

8. **Run tests after each change.** Verify your work passes before moving to the next task.
```

- [ ] **Step 6: Verify the overlay files are loadable**

Run: `ls -la config/agentic/orchestrator/prompts/overrides/`
Expected: Four `.md` files listed.

Run: `wc -l config/agentic/orchestrator/prompts/overrides/*.md`
Expected: All files have content (non-zero line counts).

- [ ] **Step 7: Commit**

```bash
git add config/agentic/orchestrator/prompts/overrides/
git commit -m "feat: add provider-specific prompt overlays for Claude and Codex"
```

---

### Task 10: Final Verification

**Files:** None (verification only)

- [ ] **Step 1: Run full test suite**

Run: `npm run test 2>&1 | tail -30`
Expected: All tests pass at ≥90% coverage thresholds.

- [ ] **Step 2: Run lint**

Run: `npm run lint 2>&1 | tail -10`
Expected: Clean (zero warnings).

- [ ] **Step 3: Run typecheck**

Run: `npm run typecheck 2>&1 | tail -10`
Expected: Clean compilation.

- [ ] **Step 4: Run validators**

Run: `npm run validate:mcp-contracts 2>&1 | tail -5`
Expected: Clean.

Run: `npm run validate:architecture 2>&1 | tail -5`
Expected: Clean.

- [ ] **Step 5: Update progress.md**

Append entries to `spec-files/progress.md` covering:

- Drop zone utility and UI integration
- Prompt overlay resolver and schema update
- Artifact stager implementation and wiring
- Provider-specific overlay prompt files

- [ ] **Step 6: Commit progress.md**

```bash
git add spec-files/progress.md
git commit -m "docs: update progress.md with drop zone and provider optimization entries"
```
