# Ordering Agent 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:** Implement a dependency-aware spec ordering agent that analyzes pending enrollment requests, determines execution order based on spec dependencies, and feeds the existing enrollment pipeline — so users can load many features simultaneously without manually managing execution ordering.

**Architecture:** Two-layer design: (1) an organizer sidecar LLM session that reads spec files from pending enrollment requests and produces a structured ordering artifact on disk, and (2) a stateless `OrganizerAgentEnrollmentScheduler` adapter that reads the artifact during per-request `schedule()` calls and translates it into `EnrollmentDecision[]`. The sidecar follows the same global-session pattern as the reconciler in `session-orchestrator.ts`.

**Tech Stack:** TypeScript, Vitest, AJV schema validation, existing supervisor infrastructure

**Spec:** `spec-files/outstanding/agentic_orchestrator_spec_ordering_agent_spec.md` (v1.2)

---

## Pre-Implementation: Fix Existing Defects

### Task 0: Commit divergence fix formatting cleanup and fix review-tab test

**Files:**

- Modify: `packages/web-dashboard/test/components/review-tab.spec.tsx:122` (already done in this session)
- Modify: `apps/control-plane/src/supervisor/run-coordinator.ts` (formatting — unstaged)
- Modify: `apps/control-plane/test/git-reconciliation-service.spec.ts` (formatting — unstaged)
- Modify: `apps/control-plane/test/kernel.coverage2.spec.ts` (formatting — unstaged)
- Modify: `apps/control-plane/test/run-coordinator.spec.ts` (formatting — unstaged)

- [ ] **Step 1: Verify review-tab test fix passes**

Run: `npx vitest run packages/web-dashboard/test/components/review-tab.spec.tsx`
Expected: 3 tests passing

- [ ] **Step 2: Stage all formatting cleanup and test fix**

```bash
git add packages/web-dashboard/test/components/review-tab.spec.tsx \
        apps/control-plane/src/supervisor/run-coordinator.ts \
        apps/control-plane/test/git-reconciliation-service.spec.ts \
        apps/control-plane/test/kernel.coverage2.spec.ts \
        apps/control-plane/test/run-coordinator.spec.ts
```

- [ ] **Step 3: Run full test suite to confirm no regressions**

Run: `npx vitest run`
Expected: 168 test files, 2403 tests passing (0 failures)

- [ ] **Step 4: Commit**

```bash
git commit -m "fix: update review-tab test assertion and format divergence fix files

The ReviewTab component button text changed from 'Approve' to
'Approve & Merge' but the test was not updated. Also commits
remaining formatting cleanup from the divergence fix work."
```

---

## Layer 2: Deterministic Adapter (build bottom-up)

### Task 1: Add `OrganizerOrderingArtifact` type and JSON schema

**Files:**

- Modify: `apps/control-plane/src/supervisor/types.ts:80-90` (add type after `RuntimeSessionsSnapshot`)
- Create: `agentic/orchestrator/schemas/organizer-ordering-artifact.schema.json`
- Test: `apps/control-plane/test/organizer-ordering-artifact.spec.ts`

- [ ] **Step 1: Write the failing test — artifact type validation**

Create `apps/control-plane/test/organizer-ordering-artifact.spec.ts`:

```typescript
import { describe, expect, it } from 'vitest';
import Ajv from 'ajv';
import { readFileSync } from 'node:fs';
import path from 'node:path';

const schema = JSON.parse(
  readFileSync(
    path.resolve('agentic/orchestrator/schemas/organizer-ordering-artifact.schema.json'),
    'utf8',
  ),
);

const ajv = new Ajv({ strict: false });

describe('OrganizerOrderingArtifact schema', () => {
  it('GIVEN_valid_complete_artifact_WHEN_validated_THEN_passes', () => {
    const artifact = {
      version: 1,
      generated_at: '2026-03-28T12:00:00.000Z',
      expires_at: '2026-03-28T12:01:00.000Z',
      completion_status: 'complete',
      backlog_fingerprint: 'sha256:abc123',
      analyzed_requests: ['req_1'],
      dependency_graph: {
        spec_a: { depends_on: [], classification: 'no_dependency_found' },
        spec_b: { depends_on: ['spec_a'], classification: 'explicit_dependency' },
      },
      ready_set: ['spec_a'],
      deferred_set: [
        { feature_id: 'spec_b', blocked_by: ['spec_a'], reason: 'explicit dependency' },
      ],
      blocked_set: [],
    };

    const validate = ajv.compile(schema);
    expect(validate(artifact)).toBe(true);
  });

  it('GIVEN_partial_completion_status_WHEN_validated_THEN_passes', () => {
    const artifact = {
      version: 1,
      generated_at: '2026-03-28T12:00:00.000Z',
      expires_at: '2026-03-28T12:01:00.000Z',
      completion_status: 'partial',
      backlog_fingerprint: 'sha256:abc123',
      analyzed_requests: [],
      dependency_graph: {},
      ready_set: [],
      deferred_set: [],
      blocked_set: [],
    };

    const validate = ajv.compile(schema);
    expect(validate(artifact)).toBe(true);
  });

  it('GIVEN_missing_required_field_WHEN_validated_THEN_fails', () => {
    const artifact = {
      version: 1,
      generated_at: '2026-03-28T12:00:00.000Z',
      // missing expires_at, completion_status, backlog_fingerprint
    };

    const validate = ajv.compile(schema);
    expect(validate(artifact)).toBe(false);
  });

  it('GIVEN_invalid_classification_WHEN_validated_THEN_fails', () => {
    const artifact = {
      version: 1,
      generated_at: '2026-03-28T12:00:00.000Z',
      expires_at: '2026-03-28T12:01:00.000Z',
      completion_status: 'complete',
      backlog_fingerprint: 'sha256:abc123',
      analyzed_requests: [],
      dependency_graph: {
        spec_a: { depends_on: [], classification: 'made_up_classification' },
      },
      ready_set: [],
      deferred_set: [],
      blocked_set: [],
    };

    const validate = ajv.compile(schema);
    expect(validate(artifact)).toBe(false);
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `npx vitest run apps/control-plane/test/organizer-ordering-artifact.spec.ts`
Expected: FAIL — schema file does not exist

- [ ] **Step 3: Create the JSON schema**

Create `agentic/orchestrator/schemas/organizer-ordering-artifact.schema.json`:

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.local/agentic/organizer-ordering-artifact.schema.json",
  "description": "Schema for the organizer sidecar ordering artifact persisted at .aop/runtime/<instanceId>/organizer-ordering.json",
  "type": "object",
  "required": [
    "version",
    "generated_at",
    "expires_at",
    "completion_status",
    "backlog_fingerprint",
    "analyzed_requests",
    "dependency_graph",
    "ready_set",
    "deferred_set",
    "blocked_set"
  ],
  "additionalProperties": false,
  "properties": {
    "version": {
      "type": "integer",
      "const": 1
    },
    "generated_at": {
      "type": "string",
      "format": "date-time"
    },
    "expires_at": {
      "type": "string",
      "format": "date-time"
    },
    "completion_status": {
      "type": "string",
      "enum": ["complete", "partial"]
    },
    "backlog_fingerprint": {
      "type": "string",
      "description": "Hash of pending request IDs + feature IDs analyzed"
    },
    "analyzed_requests": {
      "type": "array",
      "items": { "type": "string" }
    },
    "dependency_graph": {
      "type": "object",
      "additionalProperties": {
        "$ref": "#/$defs/dependencyEntry"
      }
    },
    "ready_set": {
      "type": "array",
      "items": { "type": "string" }
    },
    "deferred_set": {
      "type": "array",
      "items": { "$ref": "#/$defs/deferredEntry" }
    },
    "blocked_set": {
      "type": "array",
      "items": { "$ref": "#/$defs/blockedEntry" }
    }
  },
  "$defs": {
    "dependencyEntry": {
      "type": "object",
      "required": ["depends_on", "classification"],
      "additionalProperties": false,
      "properties": {
        "depends_on": {
          "type": "array",
          "items": { "type": "string" }
        },
        "classification": {
          "type": "string",
          "enum": [
            "explicit_dependency",
            "inferred_dependency",
            "no_dependency_found",
            "ambiguous_dependency_requires_human"
          ]
        }
      }
    },
    "deferredEntry": {
      "type": "object",
      "required": ["feature_id", "blocked_by", "reason"],
      "additionalProperties": false,
      "properties": {
        "feature_id": { "type": "string" },
        "blocked_by": {
          "type": "array",
          "items": { "type": "string" }
        },
        "reason": { "type": "string" }
      }
    },
    "blockedEntry": {
      "type": "object",
      "required": ["feature_id", "reason"],
      "additionalProperties": false,
      "properties": {
        "feature_id": { "type": "string" },
        "reason": { "type": "string" }
      }
    }
  }
}
```

- [ ] **Step 4: Add TypeScript type to `types.ts`**

Add after line 90 in `apps/control-plane/src/supervisor/types.ts` (after `RuntimeSessionsSnapshot`):

```typescript
export interface OrganizerDependencyEntry {
  depends_on: string[];
  classification:
    | 'explicit_dependency'
    | 'inferred_dependency'
    | 'no_dependency_found'
    | 'ambiguous_dependency_requires_human';
}

export interface OrganizerDeferredEntry {
  feature_id: string;
  blocked_by: string[];
  reason: string;
}

export interface OrganizerBlockedEntry {
  feature_id: string;
  reason: string;
}

export interface OrganizerOrderingArtifact {
  version: 1;
  generated_at: string;
  expires_at: string;
  completion_status: 'complete' | 'partial';
  backlog_fingerprint: string;
  analyzed_requests: string[];
  dependency_graph: Record<string, OrganizerDependencyEntry>;
  ready_set: string[];
  deferred_set: OrganizerDeferredEntry[];
  blocked_set: OrganizerBlockedEntry[];
}
```

- [ ] **Step 5: Run test to verify it passes**

Run: `npx vitest run apps/control-plane/test/organizer-ordering-artifact.spec.ts`
Expected: 4 tests passing

- [ ] **Step 6: Commit**

```bash
git add apps/control-plane/src/supervisor/types.ts \
        agentic/orchestrator/schemas/organizer-ordering-artifact.schema.json \
        apps/control-plane/test/organizer-ordering-artifact.spec.ts
git commit -m "feat: add OrganizerOrderingArtifact type and JSON schema

Defines the ordering artifact contract for the organizer sidecar,
including dependency_graph, ready_set, deferred_set, blocked_set,
artifact lifecycle fields (expires_at, completion_status, backlog_fingerprint),
and AJV-validated JSON schema."
```

---

### Task 2: Implement `OrganizerAgentEnrollmentScheduler` adapter

**Files:**

- Create: `apps/control-plane/src/supervisor/organizer-enrollment-scheduler.ts`
- Test: `apps/control-plane/test/organizer-enrollment-scheduler.spec.ts`

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

Create `apps/control-plane/test/organizer-enrollment-scheduler.spec.ts`:

```typescript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { OrganizerAgentEnrollmentScheduler } from '../src/supervisor/organizer-enrollment-scheduler.js';
import { CapacityAwareEnrollmentScheduler } from '../src/supervisor/execution-enrollment-service.js';
import type {
  EnrollmentSchedulingInput,
  OrganizerOrderingArtifact,
} from '../src/supervisor/types.js';

function makeInput(overrides: Partial<EnrollmentSchedulingInput> = {}): EnrollmentSchedulingInput {
  return {
    activeFeatureIds: [],
    queuedFeatureIds: [],
    assignedFeatureIds: [],
    maxActiveFeatures: 3,
    request: {
      request_id: 'req_1',
      request_type: 'add_features',
      status: 'pending',
      requested_at: new Date().toISOString(),
      processed_at: null,
      requested_by: 'cli:add',
      operation_id: 'op:1',
      features: [
        { feature_id: 'spec_a', spec_path: '.aop/features/spec_a/spec.md' },
        { feature_id: 'spec_b', spec_path: '.aop/features/spec_b/spec.md' },
      ],
      progress: null,
      outcome: null,
    },
    features: [
      { feature_id: 'spec_a', status: null },
      { feature_id: 'spec_b', status: null },
    ],
    ...overrides,
  };
}

function makeArtifact(
  overrides: Partial<OrganizerOrderingArtifact> = {},
): OrganizerOrderingArtifact {
  return {
    version: 1,
    generated_at: new Date().toISOString(),
    expires_at: new Date(Date.now() + 60_000).toISOString(),
    completion_status: 'complete',
    backlog_fingerprint: 'sha256:test',
    analyzed_requests: ['req_1'],
    dependency_graph: {
      spec_a: { depends_on: [], classification: 'no_dependency_found' },
      spec_b: { depends_on: ['spec_a'], classification: 'explicit_dependency' },
    },
    ready_set: ['spec_a'],
    deferred_set: [{ feature_id: 'spec_b', blocked_by: ['spec_a'], reason: 'explicit dependency' }],
    blocked_set: [],
    ...overrides,
  };
}

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

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

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

  it('GIVEN_ready_feature_in_artifact_WHEN_schedule_THEN_returns_queue_now_with_organizer_ready', async () => {
    await fs.writeFile(artifactPath, JSON.stringify(makeArtifact()), 'utf8');

    const scheduler = new OrganizerAgentEnrollmentScheduler({
      orderingArtifactPath: artifactPath,
      fallback: new CapacityAwareEnrollmentScheduler(),
    });

    const result = await scheduler.schedule(
      makeInput({ features: [{ feature_id: 'spec_a', status: null }] }),
    );
    expect(result.decisions).toHaveLength(1);
    expect(result.decisions[0]).toMatchObject({
      feature_id: 'spec_a',
      disposition: 'queue_now',
      reason: 'organizer_ready',
    });
  });

  it('GIVEN_deferred_feature_in_artifact_WHEN_schedule_THEN_returns_defer_with_dependency_blocked', async () => {
    await fs.writeFile(artifactPath, JSON.stringify(makeArtifact()), 'utf8');

    const scheduler = new OrganizerAgentEnrollmentScheduler({
      orderingArtifactPath: artifactPath,
      fallback: new CapacityAwareEnrollmentScheduler(),
    });

    const result = await scheduler.schedule(
      makeInput({ features: [{ feature_id: 'spec_b', status: null }] }),
    );
    expect(result.decisions).toHaveLength(1);
    expect(result.decisions[0]).toMatchObject({
      feature_id: 'spec_b',
      disposition: 'defer',
      reason: 'dependency_blocked',
    });
  });

  it('GIVEN_blocked_feature_in_artifact_WHEN_schedule_THEN_returns_defer_with_dependency_blocked', async () => {
    const artifact = makeArtifact({
      blocked_set: [{ feature_id: 'spec_c', reason: 'ambiguous' }],
      dependency_graph: {
        spec_c: { depends_on: ['spec_a'], classification: 'ambiguous_dependency_requires_human' },
      },
    });
    await fs.writeFile(artifactPath, JSON.stringify(artifact), 'utf8');

    const scheduler = new OrganizerAgentEnrollmentScheduler({
      orderingArtifactPath: artifactPath,
      fallback: new CapacityAwareEnrollmentScheduler(),
    });

    const result = await scheduler.schedule(
      makeInput({ features: [{ feature_id: 'spec_c', status: null }] }),
    );
    expect(result.decisions[0]).toMatchObject({
      disposition: 'defer',
      reason: 'dependency_blocked',
    });
  });

  it('GIVEN_feature_not_in_artifact_WHEN_schedule_THEN_delegates_to_fallback', async () => {
    await fs.writeFile(artifactPath, JSON.stringify(makeArtifact()), 'utf8');

    const fallback = new CapacityAwareEnrollmentScheduler();
    const scheduler = new OrganizerAgentEnrollmentScheduler({
      orderingArtifactPath: artifactPath,
      fallback,
    });

    const result = await scheduler.schedule(
      makeInput({ features: [{ feature_id: 'unknown_spec', status: null }] }),
    );
    expect(result.decisions[0]).toMatchObject({
      feature_id: 'unknown_spec',
      disposition: 'queue_now',
      reason: 'capacity_available',
    });
  });

  it('GIVEN_artifact_missing_WHEN_schedule_THEN_delegates_to_fallback', async () => {
    const scheduler = new OrganizerAgentEnrollmentScheduler({
      orderingArtifactPath: path.join(tmpDir, 'nonexistent.json'),
      fallback: new CapacityAwareEnrollmentScheduler(),
    });

    const result = await scheduler.schedule(
      makeInput({ features: [{ feature_id: 'spec_a', status: null }] }),
    );
    expect(result.decisions[0]).toMatchObject({
      disposition: 'queue_now',
      reason: 'capacity_available',
    });
  });

  it('GIVEN_artifact_with_partial_status_WHEN_schedule_THEN_delegates_to_fallback', async () => {
    const artifact = makeArtifact({ completion_status: 'partial' });
    await fs.writeFile(artifactPath, JSON.stringify(artifact), 'utf8');

    const scheduler = new OrganizerAgentEnrollmentScheduler({
      orderingArtifactPath: artifactPath,
      fallback: new CapacityAwareEnrollmentScheduler(),
    });

    const result = await scheduler.schedule(
      makeInput({ features: [{ feature_id: 'spec_a', status: null }] }),
    );
    expect(result.decisions[0].reason).toBe('capacity_available');
  });

  it('GIVEN_artifact_expired_WHEN_schedule_THEN_delegates_to_fallback', async () => {
    const artifact = makeArtifact({ expires_at: new Date(Date.now() - 1000).toISOString() });
    await fs.writeFile(artifactPath, JSON.stringify(artifact), 'utf8');

    const scheduler = new OrganizerAgentEnrollmentScheduler({
      orderingArtifactPath: artifactPath,
      fallback: new CapacityAwareEnrollmentScheduler(),
    });

    const result = await scheduler.schedule(
      makeInput({ features: [{ feature_id: 'spec_a', status: null }] }),
    );
    expect(result.decisions[0].reason).toBe('capacity_available');
  });

  it('GIVEN_corrupt_artifact_WHEN_schedule_THEN_delegates_to_fallback', async () => {
    await fs.writeFile(artifactPath, 'NOT VALID JSON{{{', 'utf8');

    const scheduler = new OrganizerAgentEnrollmentScheduler({
      orderingArtifactPath: artifactPath,
      fallback: new CapacityAwareEnrollmentScheduler(),
    });

    const result = await scheduler.schedule(
      makeInput({ features: [{ feature_id: 'spec_a', status: null }] }),
    );
    expect(result.decisions[0].reason).toBe('capacity_available');
  });

  it('GIVEN_ready_feature_but_capacity_full_WHEN_schedule_THEN_returns_defer_capacity_full', async () => {
    await fs.writeFile(artifactPath, JSON.stringify(makeArtifact()), 'utf8');

    const scheduler = new OrganizerAgentEnrollmentScheduler({
      orderingArtifactPath: artifactPath,
      fallback: new CapacityAwareEnrollmentScheduler(),
    });

    const result = await scheduler.schedule(
      makeInput({
        activeFeatureIds: ['x', 'y', 'z'],
        maxActiveFeatures: 3,
        features: [{ feature_id: 'spec_a', status: null }],
      }),
    );
    expect(result.decisions[0]).toMatchObject({
      disposition: 'defer',
      reason: 'capacity_full',
    });
  });

  it('GIVEN_mixed_features_WHEN_schedule_THEN_returns_correct_decisions_per_feature', async () => {
    await fs.writeFile(artifactPath, JSON.stringify(makeArtifact()), 'utf8');

    const scheduler = new OrganizerAgentEnrollmentScheduler({
      orderingArtifactPath: artifactPath,
      fallback: new CapacityAwareEnrollmentScheduler(),
    });

    const result = await scheduler.schedule(makeInput());

    const decisionA = result.decisions.find((d) => d.feature_id === 'spec_a');
    const decisionB = result.decisions.find((d) => d.feature_id === 'spec_b');
    expect(decisionA?.disposition).toBe('queue_now');
    expect(decisionA?.reason).toBe('organizer_ready');
    expect(decisionB?.disposition).toBe('defer');
    expect(decisionB?.reason).toBe('dependency_blocked');
  });
});
```

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

Run: `npx vitest run apps/control-plane/test/organizer-enrollment-scheduler.spec.ts`
Expected: FAIL — module not found

- [ ] **Step 3: Implement the adapter**

Create `apps/control-plane/src/supervisor/organizer-enrollment-scheduler.ts`:

```typescript
import { readFile } from 'node:fs/promises';
import type {
  EnrollmentDecision,
  EnrollmentScheduler,
  EnrollmentSchedulingInput,
  EnrollmentSchedulingResult,
  OrganizerOrderingArtifact,
} from './types.js';

interface OrganizerAgentEnrollmentSchedulerDeps {
  orderingArtifactPath: string;
  fallback: EnrollmentScheduler;
  maxArtifactAgeSecs?: number;
}

const DEFAULT_MAX_ARTIFACT_AGE_SECS = 60;

export class OrganizerAgentEnrollmentScheduler implements EnrollmentScheduler {
  private readonly orderingArtifactPath: string;
  private readonly fallback: EnrollmentScheduler;
  private readonly maxArtifactAgeSecs: number;

  constructor(deps: OrganizerAgentEnrollmentSchedulerDeps) {
    this.orderingArtifactPath = deps.orderingArtifactPath;
    this.fallback = deps.fallback;
    this.maxArtifactAgeSecs = deps.maxArtifactAgeSecs ?? DEFAULT_MAX_ARTIFACT_AGE_SECS;
  }

  async schedule(input: EnrollmentSchedulingInput): Promise<EnrollmentSchedulingResult> {
    const artifact = await this.readArtifact();
    if (!artifact) {
      return this.fallback.schedule(input);
    }

    const readySet = new Set(artifact.ready_set);
    const deferredIds = new Set(artifact.deferred_set.map((d) => d.feature_id));
    const blockedIds = new Set(artifact.blocked_set.map((b) => b.feature_id));
    const capacityAvailable = input.activeFeatureIds.length < input.maxActiveFeatures;

    const decisions: EnrollmentDecision[] = [];
    const fallbackFeatures: Array<{ feature_id: string; status: string | null }> = [];

    for (const feature of input.features) {
      const featureId = feature.feature_id;

      if (readySet.has(featureId)) {
        if (!capacityAvailable) {
          decisions.push({
            request_id: input.request.request_id,
            feature_id: featureId,
            disposition: 'defer',
            reason: 'capacity_full',
          });
        } else {
          decisions.push({
            request_id: input.request.request_id,
            feature_id: featureId,
            disposition: 'queue_now',
            reason: 'organizer_ready',
          });
        }
      } else if (deferredIds.has(featureId) || blockedIds.has(featureId)) {
        decisions.push({
          request_id: input.request.request_id,
          feature_id: featureId,
          disposition: 'defer',
          reason: 'dependency_blocked',
        });
      } else {
        fallbackFeatures.push(feature);
      }
    }

    if (fallbackFeatures.length > 0) {
      const fallbackResult = await this.fallback.schedule({
        ...input,
        features: fallbackFeatures,
      });
      decisions.push(...fallbackResult.decisions);
    }

    return { decisions };
  }

  private async readArtifact(): Promise<OrganizerOrderingArtifact | null> {
    let raw: string;
    try {
      raw = await readFile(this.orderingArtifactPath, 'utf8');
    } catch {
      console.warn('[organizer-scheduler] Artifact missing, falling back to capacity scheduler');
      return null;
    }

    let artifact: OrganizerOrderingArtifact;
    try {
      artifact = JSON.parse(raw) as OrganizerOrderingArtifact;
    } catch {
      console.warn(
        '[organizer-scheduler] Artifact unparseable, falling back to capacity scheduler',
      );
      return null;
    }

    if (artifact.completion_status === 'partial') {
      console.warn('[organizer-scheduler] Artifact partial, falling back to capacity scheduler');
      return null;
    }

    const expiresAt = new Date(artifact.expires_at).getTime();
    if (Number.isNaN(expiresAt) || expiresAt < Date.now()) {
      console.warn('[organizer-scheduler] Artifact expired, falling back to capacity scheduler');
      return null;
    }

    return artifact;
  }
}
```

- [ ] **Step 4: Run tests to verify they pass**

Run: `npx vitest run apps/control-plane/test/organizer-enrollment-scheduler.spec.ts`
Expected: 10 tests passing

- [ ] **Step 5: Run lint and typecheck**

Run: `npm run lint && npm run typecheck`
Expected: Both pass clean

- [ ] **Step 6: Commit**

```bash
git add apps/control-plane/src/supervisor/organizer-enrollment-scheduler.ts \
        apps/control-plane/test/organizer-enrollment-scheduler.spec.ts
git commit -m "feat: implement OrganizerAgentEnrollmentScheduler adapter

Stateless adapter that reads the organizer ordering artifact on each
schedule() call and translates ready/deferred/blocked sets into
EnrollmentDecision[]. Falls back to CapacityAwareEnrollmentScheduler
when artifact is missing, corrupt, partial, or expired."
```

---

## Layer 1 Support: Types, Config, and Session Infrastructure

### Task 3: Extend types for organizer session tracking

**Files:**

- Modify: `apps/control-plane/src/supervisor/types.ts:80-90,117-121,137-151,261-269`

- [ ] **Step 1: Add `organizer_session_id` to `RuntimeSessionsSnapshot`**

In `apps/control-plane/src/supervisor/types.ts`, add after `reconciler_session_id` (line ~86):

```typescript
  organizer_session_id?: string;
```

- [ ] **Step 2: Add `UpdateOrganizerSessionInput` type**

Add after `UpdateReconcilerSessionInput` (line ~121):

```typescript
export interface UpdateOrganizerSessionInput {
  run_id: string;
  owner_instance_id: string;
  organizer_session_id: string;
}
```

- [ ] **Step 3: Add `updateOrganizerSession` to `FeatureOrchestrationPort`**

Add after `updateReconcilerSession` in `FeatureOrchestrationPort` (line ~149):

```typescript
  updateOrganizerSession(
    params: UpdateOrganizerSessionInput,
  ): Promise<{ data: { runtime_sessions: RuntimeSessionsSnapshot } }>;
```

- [ ] **Step 4: Add `organizerSessionId` to `SupervisorRuntimeState`**

Add after `reconcilerSessionId` (line ~265):

```typescript
  organizerSessionId?: string | null;
```

- [ ] **Step 5: Add `organizer` to `PromptBundle`**

Add after `reconciler` (line ~71):

```typescript
  organizer?: string | null;
```

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

Run: `npm run typecheck`
Expected: Errors for unimplemented `updateOrganizerSession` in kernel — expected at this stage, will be implemented in Task 4

- [ ] **Step 7: Commit**

```bash
git add apps/control-plane/src/supervisor/types.ts
git commit -m "feat: add organizer session types to supervisor type definitions

Adds organizer_session_id to RuntimeSessionsSnapshot,
UpdateOrganizerSessionInput, updateOrganizerSession to
FeatureOrchestrationPort, organizerSessionId to
SupervisorRuntimeState, and organizer to PromptBundle."
```

---

### Task 4: Implement `updateOrganizerSession` in kernel and run-lease-service

**Files:**

- Modify: `apps/control-plane/src/application/services/run-lease-service.ts:420+`
- Modify: `apps/control-plane/src/core/kernel.ts:798+`
- Test: `apps/control-plane/test/kernel.coverage2.spec.ts` (add organizer session test)

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

Add to `apps/control-plane/test/kernel.coverage2.spec.ts` in the session management describe block:

```typescript
it('GIVEN_active_run_WHEN_updateOrganizerSession_called_THEN_persists_organizer_session_id', async () => {
  const kernel = new AopKernel(repoRoot);
  await kernel.ensureLoaded();

  await kernel.acquireRunLease({
    run_id: 'run:org-test',
    owner_instance_id: 'owner:org-test',
    provider: 'custom',
    model: 'test-model',
    provider_config_ref_hash: 'hash',
  });

  const result = await kernel.updateOrganizerSession({
    run_id: 'run:org-test',
    owner_instance_id: 'owner:org-test',
    organizer_session_id: 'organizer:session-1',
  });

  expect(result.data.runtime_sessions.organizer_session_id).toBe('organizer:session-1');
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `npx vitest run apps/control-plane/test/kernel.coverage2.spec.ts -t "updateOrganizerSession"`
Expected: FAIL — method does not exist

- [ ] **Step 3: Implement `updateOrganizerSession` in run-lease-service**

Add after `updateReconcilerSession` in `apps/control-plane/src/application/services/run-lease-service.ts` (~line 460):

```typescript
  async updateOrganizerSession(
    params: RuntimeOrganizerSessionUpdateParams,
  ): Promise<{ data: { runtime_sessions: RuntimeSessionsSnapshot } }> {
    const { run_id, owner_instance_id, organizer_session_id } = params;
    if (!run_id || !owner_instance_id || !organizer_session_id) {
      throw {
        normalizedResponse: fail(
          ERROR_CODES.INVALID_ARGUMENT,
          'run_id, owner_instance_id, and organizer_session_id are required',
          {
            retryable: false,
            requires_human: true,
          },
        ),
      };
    }

    return await this.port.withIndexLock(async () => {
      const current = this.port.normalizeRuntimeSessions(await this.port.readRunLease());
      if (current.run_id !== run_id || current.owner_instance_id !== owner_instance_id) {
        throw {
          normalizedResponse: fail(
            ERROR_CODES.RUN_LEASE_NOT_OWNED,
            'Cannot update organizer session without run ownership',
            {
              run_id,
              owner_instance_id,
              active_run_id: current.run_id,
              active_owner_instance_id: current.owner_instance_id,
              retryable: false,
              requires_human: true,
            },
          ),
        };
      }

      const updated: RuntimeSessionsSnapshot = {
        ...current,
        organizer_session_id,
      };

      await this.port.writeRunLease(updated);
      return { data: { runtime_sessions: updated } };
    });
  }
```

Also add the params type near the other runtime param types in the same file:

```typescript
interface RuntimeOrganizerSessionUpdateParams {
  run_id: string;
  owner_instance_id: string;
  organizer_session_id: string;
}
```

- [ ] **Step 4: Add kernel delegation method**

Add after `updateReconcilerSession` in `apps/control-plane/src/core/kernel.ts` (~line 804):

```typescript
  async updateOrganizerSession(params: {
    run_id: string;
    owner_instance_id: string;
    organizer_session_id: string;
  }): Promise<{ data: { runtime_sessions: RuntimeSessionsSnapshot } }> {
    return await this.runLeaseService.updateOrganizerSession(params);
  }
```

- [ ] **Step 5: Run test to verify it passes**

Run: `npx vitest run apps/control-plane/test/kernel.coverage2.spec.ts -t "updateOrganizerSession"`
Expected: PASS

- [ ] **Step 6: Run full typecheck and lint**

Run: `npm run typecheck && npm run lint`
Expected: Both pass clean

- [ ] **Step 7: Commit**

```bash
git add apps/control-plane/src/application/services/run-lease-service.ts \
        apps/control-plane/src/core/kernel.ts \
        apps/control-plane/test/kernel.coverage2.spec.ts
git commit -m "feat: implement updateOrganizerSession in kernel and run-lease-service

Follows the same pattern as updateReconcilerSession: validates
ownership, acquires index lock, persists organizer_session_id
to RuntimeSessionsSnapshot."
```

---

### Task 5: Add organizer to agents config, schema, and prompt loader

**Files:**

- Modify: `config/agentic/orchestrator/agents.yaml`
- Modify: `agentic/orchestrator/schemas/agents.schema.json:227-249`
- Modify: `apps/control-plane/src/supervisor/prompt-bundle-loader.ts:30`
- Create: `config/agentic/orchestrator/prompts/organizer.system.md`
- Test: Existing schema validation tests cover agents.yaml

- [ ] **Step 1: Add organizer role to `agents.yaml`**

Add after the reconciler entry in `config/agentic/orchestrator/agents.yaml`:

```yaml
organizer:
  system_prompt_path: config/agentic/orchestrator/prompts/organizer.system.md
  description: Analyzes pending specs to determine dependency-aware execution ordering
  capabilities:
    - dependency_analysis
    - spec_ordering
    - backlog_prioritization
```

- [ ] **Step 2: Add organizer to `agents.schema.json` provider_overrides**

In `agentic/orchestrator/schemas/agents.schema.json`, add to the `role_provider_overrides` properties (after the reconciler entry):

```json
            "organizer": {
              "$ref": "#/$defs/roleProviderOverride",
              "description": "Overrides for the organizer role."
            }
```

- [ ] **Step 3: Add `'organizer'` to prompt-bundle-loader role iteration**

In `apps/control-plane/src/supervisor/prompt-bundle-loader.ts`, change line 30:

From:

```typescript
    for (const role of ['planner', 'builder', 'qa', 'reconciler'] as const) {
```

To:

```typescript
    for (const role of ['planner', 'builder', 'qa', 'reconciler', 'organizer'] as const) {
```

Also update the initial `prompts` object (around line 28):

From:

```typescript
const prompts: PromptBundle = {
  planner: null,
  builder: null,
  qa: null,
  reconciler: null,
};
```

To:

```typescript
const prompts: PromptBundle = {
  planner: null,
  builder: null,
  qa: null,
  reconciler: null,
  organizer: null,
};
```

- [ ] **Step 4: Create the organizer system prompt**

Create `config/agentic/orchestrator/prompts/organizer.system.md`:

````markdown
You are the organizer role.

Your job is to analyze pending feature specs and determine the optimal execution ordering based on dependency relationships. You do NOT implement features — you decide which features can start now and which must wait.

## Organizer objective

Read all pending spec files, identify dependency relationships between them, and produce a structured ordering artifact that the enrollment scheduler uses to admit features in the correct order.

## Analysis discipline

For each pending spec:

1. Read the full spec content from the provided spec_path
2. Check for explicit `Depends On:` declarations in frontmatter and body
3. Identify shared file paths or overlapping areas that suggest implicit dependencies
4. If semantic analysis suggests a dependency but confidence is low, classify as `ambiguous_dependency_requires_human` rather than guessing

## Dependency classification

Every dependency edge MUST be classified as exactly one of:

- `explicit_dependency` — declared in spec metadata or body (e.g. `Depends On:` field)
- `inferred_dependency` — derived from your semantic analysis of spec content and shared code areas
- `no_dependency_found` — specs analyzed and found to be independent
- `ambiguous_dependency_requires_human` — confidence too low to decide; escalate rather than guess

## Cycle detection

If you detect a dependency cycle (A depends on B, B depends on A), report all features in the cycle in the `blocked_set` with a clear explanation of the cycle.

## Output format

Return exactly one JSON object. Do not wrap in markdown code fences.

```json
{
  "type": "ORDERING",
  "ordering": {
    "dependency_graph": {
      "<feature_id>": { "depends_on": ["<dep_id>"], "classification": "<classification>" }
    },
    "ready_set": ["<feature_ids with no unresolved deps>"],
    "deferred_set": [
      { "feature_id": "<id>", "blocked_by": ["<dep_ids>"], "reason": "<explanation>" }
    ],
    "blocked_set": [{ "feature_id": "<id>", "reason": "<explanation>" }]
  }
}
```
````

## Constraints

- Never invent dependencies where none exist — independent features should run in parallel
- Never silently drop a `Depends On:` declaration — explicit dependencies are authoritative
- Do not attempt to implement, plan, or modify any feature
- If unsure whether a dependency is real, classify as `ambiguous_dependency_requires_human`

````

- [ ] **Step 5: Run typecheck and lint**

Run: `npm run typecheck && npm run lint`
Expected: Both pass clean

- [ ] **Step 6: Commit**

```bash
git add config/agentic/orchestrator/agents.yaml \
        config/agentic/orchestrator/prompts/organizer.system.md \
        agentic/orchestrator/schemas/agents.schema.json \
        apps/control-plane/src/supervisor/prompt-bundle-loader.ts
git commit -m "feat: add organizer role config, prompt, schema, and prompt loading

Adds organizer entry to agents.yaml (same pattern as reconciler),
organizer system prompt for spec dependency analysis, organizer to
agents.schema.json provider_overrides, and 'organizer' to the
prompt-bundle-loader role iteration list."
````

---

### Task 6: Add `ensureGlobalOrganizerSession()` to SessionOrchestrator

**Files:**

- Modify: `apps/control-plane/src/supervisor/session-orchestrator.ts:98-136`
- Test: `apps/control-plane/test/session-management.spec.ts`

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

Add to `apps/control-plane/test/session-management.spec.ts`:

```typescript
describe('organizer session lifecycle', () => {
  it('GIVEN_no_previous_organizer_session_WHEN_ensureGlobalOrchestratorSession_THEN_creates_organizer_session', async () => {
    const repoRoot = await makeTempRepo(process.cwd());
    const kernel = new AopKernel(repoRoot);
    await kernel.ensureLoaded();

    await kernel.acquireRunLease({
      run_id: 'run:org-session',
      owner_instance_id: 'owner:org-session',
      provider: 'custom',
      model: 'test-model',
      provider_config_ref_hash: 'hash',
    });

    const provider = makeProvider({
      createSession: vi.fn(async (role: string) => ({
        session_id: `${role}:session-1`,
      })),
      reattachSession: vi.fn(async () => null),
    });

    const state: SupervisorRuntimeState = {
      runId: 'run:org-session',
      ownerInstanceId: 'owner:org-session',
      orchestratorSessionId: null,
      reconcilerSessionId: null,
      organizerSessionId: null,
      sessionsByFeature: new Map(),
      queue: [],
      runMetadata: {},
    };

    const sessionOrchestrator = new SessionOrchestrator({
      kernel: kernel as never,
      runtimeStateReader: kernel as never,
      provider: provider as never,
      promptProvider: {
        loadRolePrompts: async () => ({
          planner: null,
          builder: null,
          qa: null,
          reconciler: null,
          organizer: null,
        }),
      } as never,
      toolCaller: { callTool: vi.fn(async () => ({ ok: true, data: {} })) } as never,
      state: state as never,
    });

    await sessionOrchestrator.ensureGlobalOrchestratorSession();

    expect(state.organizerSessionId).toBe('organizer:session-1');
    expect(provider.createSession).toHaveBeenCalledWith('organizer', 'global', null);

    const sessions = await kernel.getRuntimeSessions();
    expect(sessions.organizer_session_id).toBe('organizer:session-1');
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `npx vitest run apps/control-plane/test/session-management.spec.ts -t "organizer session lifecycle"`
Expected: FAIL — `ensureGlobalOrganizerSession` not called / `organizerSessionId` is null

- [ ] **Step 3: Add `ensureGlobalOrganizerSession()` to SessionOrchestrator**

Add after `ensureGlobalReconcilerSession()` in `apps/control-plane/src/supervisor/session-orchestrator.ts` (after line 136):

```typescript
  private async ensureGlobalOrganizerSession(): Promise<void> {
    const runtimeSessions = await this.runtimeStateReader.getRuntimeSessions();
    const candidate = runtimeSessions.organizer_session_id;
    const canReuse =
      runtimeSessions.run_id === this.state.runId &&
      runtimeSessions.owner_instance_id === this.state.ownerInstanceId &&
      runtimeSessions.provider === this.provider.selection.provider &&
      typeof candidate === 'string' &&
      candidate.length > 0 &&
      candidate !== 'unknown';

    if (canReuse) {
      const reattachSucceeded = await this.attemptReattach(candidate);
      if (reattachSucceeded) {
        this.state.organizerSessionId = candidate;
        this.state.runMetadata.organizer_reattached = true;
        return;
      }
    }

    const prompts = await this.promptProvider.loadRolePrompts();
    const organizerSession = await this.provider.createSession(
      'organizer',
      'global',
      prompts.organizer ?? null,
    );
    this.state.organizerSessionId = organizerSession.session_id;
    this.state.runMetadata.organizer_reattached = false;

    await this.kernel.updateOrganizerSession({
      run_id: this.state.runId,
      owner_instance_id: this.state.ownerInstanceId,
      organizer_session_id: organizerSession.session_id,
    });

    if (canReuse && candidate) {
      await this.provider.closeSession(candidate).catch(() => ({ closed: true }));
    }
  }
```

Then add `await this.ensureGlobalOrganizerSession();` at the end of both paths in `ensureGlobalOrchestratorSession()` — after the `ensureGlobalReconcilerSession()` calls on lines 75 and 95:

```typescript
await this.ensureGlobalReconcilerSession();
await this.ensureGlobalOrganizerSession();
```

- [ ] **Step 4: Run test to verify it passes**

Run: `npx vitest run apps/control-plane/test/session-management.spec.ts -t "organizer session lifecycle"`
Expected: PASS

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

Run: `npx vitest run`
Expected: All tests passing

- [ ] **Step 6: Commit**

```bash
git add apps/control-plane/src/supervisor/session-orchestrator.ts \
        apps/control-plane/test/session-management.spec.ts
git commit -m "feat: add ensureGlobalOrganizerSession to SessionOrchestrator

Creates/reattaches the organizer sidecar session following the same
pattern as ensureGlobalReconcilerSession. Called during orchestrator
session setup alongside the reconciler."
```

---

## Wiring: Connect the Layers

### Task 7: Wire `OrganizerAgentEnrollmentScheduler` into runtime startup

**Files:**

- Modify: `apps/control-plane/src/supervisor/runtime.ts:512-517`
- Modify: `apps/control-plane/src/core/path-layout.ts:48-50`

- [ ] **Step 1: Add organizer artifact path to PathLayout**

In `apps/control-plane/src/core/path-layout.ts`, add after `executionControlPath`:

```typescript
  organizerOrderingPath(instanceId: string): string {
    return path.join(this.instanceRuntimeRoot(instanceId), 'organizer-ordering.json');
  }
```

- [ ] **Step 2: Wire the organizer scheduler in runtime.ts**

Replace the `CapacityAwareEnrollmentScheduler` instantiation in `apps/control-plane/src/supervisor/runtime.ts` (line ~515):

From:

```typescript
this.executionEnrollmentService = new ExecutionEnrollmentService({
  kernel: this.kernel,
  state: this.state,
  scheduler: new CapacityAwareEnrollmentScheduler(),
  maxActiveFeatures: this.maxActiveFeatures,
});
```

To:

```typescript
const capacityScheduler = new CapacityAwareEnrollmentScheduler();
const organizerEnabled = this.kernel.getAgentsConfig().roles?.organizer != null;
const scheduler: EnrollmentScheduler = organizerEnabled
  ? new OrganizerAgentEnrollmentScheduler({
      orderingArtifactPath: this.kernel
        .getPathLayout()
        .organizerOrderingPath(this.state.ownerInstanceId),
      fallback: capacityScheduler,
    })
  : capacityScheduler;

this.executionEnrollmentService = new ExecutionEnrollmentService({
  kernel: this.kernel,
  state: this.state,
  scheduler,
  maxActiveFeatures: this.maxActiveFeatures,
});
```

Add imports at the top of runtime.ts:

```typescript
import { OrganizerAgentEnrollmentScheduler } from './organizer-enrollment-scheduler.js';
```

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

Run: `npm run typecheck && npm run lint`
Expected: Both pass clean

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

Run: `npx vitest run`
Expected: All tests passing (existing tests use mocked enrollment service so this wiring doesn't affect them)

- [ ] **Step 5: Commit**

```bash
git add apps/control-plane/src/supervisor/runtime.ts \
        apps/control-plane/src/core/path-layout.ts
git commit -m "feat: wire OrganizerAgentEnrollmentScheduler into supervisor startup

Conditionally uses OrganizerAgentEnrollmentScheduler when the
organizer role is configured in agents.yaml, falling back to
CapacityAwareEnrollmentScheduler when not configured."
```

---

### Task 8: Add terminal-state trigger for enrollment re-evaluation

**Files:**

- Modify: `apps/control-plane/src/supervisor/run-coordinator.ts:1036-1041`
- Test: `apps/control-plane/test/run-coordinator.spec.ts`

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

Add to `apps/control-plane/test/run-coordinator.spec.ts`:

```typescript
it('GIVEN_feature_reaches_merged_WHEN_status_transitions_THEN_calls_notifyNewRequestHint', async () => {
  vi.useFakeTimers();
  try {
    let callCount = 0;
    const statusByFeature: Record<string, string> = {
      feature_trigger: STATUS.READY_TO_MERGE,
    };

    const executionEnrollmentService = {
      start: vi.fn(async () => undefined),
      stop: vi.fn(async () => undefined),
      notifyNewRequestHint: vi.fn(async () => {
        callCount += 1;
      }),
      noteSafeCheckpoint: vi.fn(async () => undefined),
      hasPendingWork: vi.fn(() => false),
      drainReadyDecisions: vi.fn(async () => []),
    };

    const kernel = {
      ensureLoaded: vi.fn(async () => undefined),
      recoverFromState: vi.fn(async () => ({ data: { recovered: true } })),
      acquireRunLease: vi.fn(async () => ({ data: { took_over_stale: false } })),
      releaseRunLease: vi.fn(async () => ({ data: { released: true } })),
      pruneFeatureSessionAssignments: vi.fn(async () => ({ data: { removed: [] as string[] } })),
      getRuntimeSessions: vi.fn(async () => ({
        run_id: 'run:test',
        owner_instance_id: 'owner:test',
        orchestrator_session_id: 'orch:test',
        feature_sessions: {},
      })),
      getPolicySnapshot: vi.fn(() => ({
        reactions: {
          gate_failed: { enabled: true, action: 'retry_with_agent_repair', max_retries: 2 },
        },
      })),
      updateState: vi.fn(async () => ({})),
      checkBudget: vi.fn(async () => ({
        over_budget: false,
        alert_threshold_reached: false,
        current_cost_usd: 0,
        limit_usd: -1,
        alert_threshold: 0.8,
      })),
      drainPendingExecutionRequests: vi.fn(async () => ({ data: { items: [] } })),
      markExecutionRequestProcessed: vi.fn(async () => ({ data: { updated: true } })),
    };

    const toolCaller = {
      callTool: vi.fn(async (_role: string, toolName: string) => {
        if (toolName === TOOLS.REPORT_DASHBOARD) return { ok: true, data: { features: [] } };
        if (toolName === TOOLS.FEATURE_STATE_GET) {
          return { ok: true, data: { front_matter: { status: statusByFeature.feature_trigger } } };
        }
        if (toolName === TOOLS.FEATURE_MERGE) {
          statusByFeature.feature_trigger = STATUS.MERGED;
          return { ok: true, data: { merged: true } };
        }
        return { ok: true, data: {} };
      }),
    };

    const notifier = { notify: vi.fn(async () => undefined) };

    const state = {
      runId: 'run:test',
      ownerInstanceId: 'owner:test',
      orchestratorSessionId: 'orch:test',
      sessionsByFeature: new Map(),
      queue: [] as Array<{ feature_id: string }>,
      runMetadata: {},
    };

    const sessionOrchestrator = {
      ensureGlobalOrchestratorSession: vi.fn(async () => undefined),
      cleanupOrphanWorkerSessions: vi.fn(async () => undefined),
      initializeFeatureCluster: vi.fn(async (fid: string) => {
        state.sessionsByFeature.set(fid, {
          planner: `${fid}:p`,
          builder: `${fid}:b`,
          qa: `${fid}:q`,
        });
      }),
      closeFeatureCluster: vi.fn(async () => undefined),
      reconcileQueuedFeatures: vi.fn(async () => undefined),
      enforceActiveFeatureInvariant: vi.fn(async () => undefined),
    };

    const planningWaveExecutor = {
      run: vi.fn(async () => {
        statusByFeature.feature_trigger = STATUS.MERGED;
      }),
      runPostQaReconciliation: vi.fn(async () => undefined),
      clearFeatureTracking: vi.fn(),
    };

    const coordinator = new RunCoordinator({
      kernel: kernel as never,
      provider: { selection: { provider: 'custom', model: 'test' } } as never,
      toolCaller: toolCaller as never,
      state: state as never,
      sessionOrchestrator: sessionOrchestrator as never,
      planningWaveExecutor: planningWaveExecutor as never,
      buildWaveExecutor: {
        run: vi.fn(async () => undefined),
        clearFeatureTracking: vi.fn(),
      } as never,
      qaWaveExecutor: { run: vi.fn(async () => undefined), clearFeatureTracking: vi.fn() } as never,
      leaseHeartbeatService: { renew: vi.fn(async () => undefined) } as never,
      maxActiveFeatures: 1,
      maxParallelGateRuns: 2,
      maxIterationsPerPhase: 1,
      takeoverStaleRun: false,
      providerConfigRefHash: () => 'hash',
      executionEnrollmentService: executionEnrollmentService as never,
      notifier: notifier as never,
    });

    await coordinator.start([{ feature_id: 'feature_trigger' }]);
    await vi.advanceTimersByTimeAsync(5000);

    // notifyNewRequestHint is called at startup + once on terminal state
    expect(callCount).toBeGreaterThanOrEqual(2);
  } finally {
    vi.useRealTimers();
  }
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `npx vitest run apps/control-plane/test/run-coordinator.spec.ts -t "notifyNewRequestHint"`
Expected: FAIL — callCount is only 1 (startup only)

- [ ] **Step 3: Add terminal-state trigger in run-coordinator**

In `apps/control-plane/src/supervisor/run-coordinator.ts`, find the merged status handling (around line 1036):

```typescript
        } else if (newStatus === STATUS.MERGED) {
          await this.notifier.notify('feature_merged', {
            feature_id: featureId,
            message: `Feature ${featureId} was merged`,
          });
```

Add after the notifier call:

```typescript
await this.executionEnrollmentService.notifyNewRequestHint();
```

Also find any other terminal status handling in the same method for `STATUS.FAILED` and `STATUS.PAUSED_BUDGET`, and add the same call. If there's a general terminal-status check, add a single call there instead. The key code to add after any transition to a terminal enrollment status:

```typescript
if (RunCoordinator.ENROLLMENT_TERMINAL_STATUSES.has(newStatus)) {
  await this.executionEnrollmentService.notifyNewRequestHint();
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `npx vitest run apps/control-plane/test/run-coordinator.spec.ts -t "notifyNewRequestHint"`
Expected: PASS

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

Run: `npx vitest run`
Expected: All tests passing

- [ ] **Step 6: Commit**

```bash
git add apps/control-plane/src/supervisor/run-coordinator.ts \
        apps/control-plane/test/run-coordinator.spec.ts
git commit -m "feat: trigger enrollment re-evaluation on terminal state transitions

When a feature reaches a terminal enrollment status (merged, failed,
paused_budget, ready_to_merge), calls notifyNewRequestHint() so the
enrollment service re-evaluates deferred features that may now be
unblocked."
```

---

### Task 9: Add organizer path layout method to `path-layout.ts`

This was partially done in Task 7. Verify it's working by adding a test.

**Files:**

- Test: `apps/control-plane/test/organizer-ordering-artifact.spec.ts` (add path layout test)

- [ ] **Step 1: Add path layout test to existing artifact spec**

Add to `apps/control-plane/test/organizer-ordering-artifact.spec.ts`:

```typescript
import { PathLayout } from '../src/core/path-layout.js';

describe('OrganizerOrderingArtifact path', () => {
  it('GIVEN_instance_id_WHEN_organizerOrderingPath_THEN_returns_correct_path', () => {
    const layout = new PathLayout('/repo/.aop');
    const result = layout.organizerOrderingPath('default');
    expect(result).toBe('/repo/.aop/runtime/default/organizer-ordering.json');
  });
});
```

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

Run: `npx vitest run apps/control-plane/test/organizer-ordering-artifact.spec.ts`
Expected: PASS

- [ ] **Step 3: Commit**

```bash
git add apps/control-plane/test/organizer-ordering-artifact.spec.ts
git commit -m "test: add path layout test for organizer ordering artifact"
```

---

### Task 10: Update dashboard types and status route

**Files:**

- Modify: `packages/web-dashboard/src/lib/types.ts`
- Modify: `packages/web-dashboard/src/app/api/status/route.ts`

- [ ] **Step 1: Add organizer types to dashboard**

In `packages/web-dashboard/src/lib/types.ts`, add:

```typescript
export interface OrganizerDeferredSummary {
  feature_id: string;
  blocked_by: string[];
  reason: string;
}

export interface OrganizerBlockedSummary {
  feature_id: string;
  reason: string;
}

export interface OrganizerStatusProjection {
  available: boolean;
  generated_at: string | null;
  ready_set: string[];
  deferred_set: OrganizerDeferredSummary[];
  blocked_set: OrganizerBlockedSummary[];
}
```

- [ ] **Step 2: Expose organizer status in the status route**

In `packages/web-dashboard/src/app/api/status/route.ts`, find where the status response is constructed and add an `organizer` field that reads the ordering artifact from the `.aop/runtime/<instanceId>/organizer-ordering.json` path. If the file doesn't exist, return `{ available: false, generated_at: null, ready_set: [], deferred_set: [], blocked_set: [] }`.

Read the file path and parse logic following the existing pattern in the route for reading runtime files. The exact implementation depends on the current route structure — read it first.

- [ ] **Step 3: Run dashboard tests**

Run: `npx vitest run packages/web-dashboard/test/`
Expected: All dashboard tests passing

- [ ] **Step 4: Commit**

```bash
git add packages/web-dashboard/src/lib/types.ts \
        packages/web-dashboard/src/app/api/status/route.ts
git commit -m "feat: expose organizer ordering status in dashboard API

Adds OrganizerStatusProjection type and surfaces ready/deferred/blocked
sets from the organizer ordering artifact in the status endpoint."
```

---

### Task 11: Update `progress.md` with all completed entries

**Files:**

- Modify: `spec-files/progress.md`

- [ ] **Step 1: Append numbered entries for each task completed**

Follow the format in CLAUDE.md. Each task gets its own entry with Goal, Changes made, and Result sections. Use sequential numbering from the last entry in progress.md.

- [ ] **Step 2: Commit**

```bash
git add spec-files/progress.md
git commit -m "docs: update progress.md with ordering agent implementation entries"
```

---

### Task 12: Final verification

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

Run: `npx vitest run`
Expected: All tests passing, coverage above 90% thresholds

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

Run: `npm run lint`
Expected: Zero warnings

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

Run: `npm run typecheck`
Expected: Clean compilation

- [ ] **Step 4: Run architecture validator**

Run: `npm run validate:architecture`
Expected: Silent pass

- [ ] **Step 5: Verify organizer is opt-in**

Confirm that when `agents.yaml` does NOT have an `organizer` role entry, the system uses `CapacityAwareEnrollmentScheduler` as before (no behavioral change for existing users).

---

## Task Dependency Summary

```
Task 0 (defect fixes) → independent, do first
Task 1 (types + schema) → independent foundation
Task 2 (adapter) → depends on Task 1
Task 3 (session types) → independent, parallel with Task 1
Task 4 (kernel session) → depends on Task 3
Task 5 (config + prompt) → depends on Task 3 (PromptBundle type)
Task 6 (session orchestrator) → depends on Tasks 4, 5
Task 7 (runtime wiring) → depends on Tasks 2, 6
Task 8 (terminal trigger) → independent, can parallel with Tasks 5-7
Task 9 (path test) → depends on Task 7
Task 10 (dashboard) → depends on Task 1 (types)
Task 11 (progress.md) → after all implementation tasks
Task 12 (final verification) → after everything
```

---

## Scope Note: Organizer Sidecar Runtime (Layer 1)

This plan covers the **full infrastructure** for the ordering agent: types, schemas, adapter, session lifecycle, runtime wiring, triggers, config, prompt, and dashboard integration. After this plan is complete, the system will:

- Create and manage an organizer LLM session alongside the reconciler
- Read ordering artifacts from disk and translate them into enrollment decisions
- Fall back to capacity-aware scheduling when no artifact exists
- Re-evaluate deferred features when blocking dependencies reach terminal states

**Not included in this plan:** The organizer sidecar's runtime loop that actually invokes the LLM session to analyze spec files and write the ordering artifact to disk. This is the "brain" of Layer 1 — it requires:

1. Reading pending `ExecutionRequestFeature` spec files from disk
2. Sending them to the organizer session via the provider abstraction
3. Parsing the LLM's structured ordering response
4. Writing the ordering artifact atomically to `.aop/runtime/<instanceId>/organizer-ordering.json`
5. Re-running analysis on trigger events

This will be a follow-up plan once the infrastructure from this plan is in place, as it depends on the provider interaction patterns and the organizer session being fully wired.
