# Feature Spec: Deterministic Gate Selection (System-Owned, Prompt-Independent)

> **Purpose of this document**: Define an implementation-ready design that removes planner prompt dependence for gate profile selection and guarantees deterministic gate behavior across planner, builder, and QA phases.

**Version:** 1.0  
**Date:** 2026-03-05  
**Status:** Draft  
**Roadmap Mapping:** M49

---

## 0. Standards and Dependencies

### 0.1 Required Standards

All implementation MUST preserve:

- deterministic orchestration state transitions and normalized error envelopes
- MCP/in-process parity for tool semantics
- role-based tool authorization and mutating `operation_id` idempotency semantics
- canonical runtime artifact ownership under `.aop/**`
- compatibility with existing plan/state artifacts during migration window
- quality gates: `npm run lint`, `npm run typecheck`, `npm test` with >=90% thresholds

### 0.2 Required Upstream Inputs

Implementing agents MUST read:

- `apps/control-plane/src/application/services/plan-service.ts`
- `apps/control-plane/src/application/services/patch-service.ts`
- `apps/control-plane/src/application/services/gate-service.ts`
- `apps/control-plane/src/supervisor/worker-decision-loop.ts`
- `apps/control-plane/src/core/kernel.ts`
- `apps/control-plane/src/core/error-codes.ts`
- `agentic/orchestrator/schemas/plan.schema.json`
- `agentic/orchestrator/schemas/state.schema.json`
- `agentic/orchestrator/schemas/gates.schema.json`
- `agentic/orchestrator/schemas/policy.schema.json`
- `agentic/orchestrator/policy.yaml`
- `agentic/orchestrator/prompts/planner.system.md`
- `apps/control-plane/test/plan-service.spec.ts`
- `apps/control-plane/test/patch-service.spec.ts`
- `apps/control-plane/test/gates.spec.ts`
- `apps/control-plane/test/supervisor.unit.spec.ts`

### 0.3 Scope

This spec implements:

- system-owned gate profile selection (planner cannot authoritatively set profile)
- deterministic profile resolution and normalization service
- backward-compatible canonicalization for legacy plan artifacts
- automatic healing of invalid `state.gate_profile` values
- prompt and schema alignment so planner contract no longer induces profile/mode confusion

Out of scope:

- redesign of gate execution engine (`core/gates.ts`) semantics
- introducing per-feature gate-profile overrides via new CLI flags (can be follow-up)
- changing required gate modes policy (`required_modes`, `required_merge_mode`)

### 0.4 Architecture Guardrails (Mandatory)

Implementation MUST follow:

- **KISS**: one central gate-selection resolver, no duplicated ad hoc profile checks
- **SRP**: profile resolution/canonicalization logic lives in dedicated service module
- **SOLID-oriented design**:
  - open/closed for future override sources (CLI, project config) without touching patch/gate services
  - interface segregation between resolver and consumers (plan service, patch service, kernel init)
  - dependency inversion from services to resolver port
- **Hexagonal alignment**: resolver in application/core boundary, prompt text only advisory
- **Composition over inheritance**: plan/patch/kernel compose resolver, no class hierarchy changes

---

## 1. Problem Statement and Failure Analysis

### 1.1 Failure Class

A run failed with:

- `tool_name = repo.apply_patch`
- `error.code = internal_error`
- `message = Cannot read properties of undefined (reading 'modes')`

Root trigger: `plan.gate_profile` held a **mode** token (`full`) rather than a **profile** token (`default`), causing downstream profile lookup to return `undefined`.

### 1.2 Why This Happens Today

Current contracts allow an ambiguity:

- `gates.yaml` defines **profiles** containing **modes**
- `plan.schema.json` requires planner-authored `gate_profile` (free string)
- planner prompt historically used examples like `"fast"`/`"full"` for `gate_profile`

This made planner prompt wording an execution-critical dependency.

### 1.3 Why Prompt-Only Guidance Is Insufficient

Prompt instructions are non-authoritative and probabilistic. Even a corrected prompt does not provide hard guarantees against:

- model drift or truncation
- legacy plan artifacts already persisted
- alternate provider formatting behavior
- manual plan edits

The runtime must be deterministic without trusting prompt compliance.

---

## 2. Architectural Critique of the Current Hotfix

Current guardrails (reject invalid profile in `plan.submit` and fail fast in `repo.apply_patch`) are necessary but incomplete.

What they solve:

- convert hard crash into typed failure
- prevent null dereference at patch time

What they do not solve:

1. planner remains the authoritative source for a system-level control (`gate_profile`)
2. legacy feature state may still carry invalid `state.gate_profile`
3. builder/QA still depend on plan-originated profile in some paths unless fully decoupled
4. repeated planner retries can still loop on a semantically wrong profile token

Conclusion: the hotfix is a safety net, not a deterministic architecture.

---

## 3. Deterministic Target Design

## 3.1 Source-of-Truth Rule

`state.gate_profile` is the only authoritative gate profile for a feature.

Planner output is not authoritative for profile selection.

### 3.1.1 Ownership Model

- **System-owned field**: `state.gate_profile`
- **Planner-owned field**: `plan.gate_targets` (optional mode subset only)
- **Policy-owned default**: `policy.gate_selection.default_profile`

### 3.1.2 Runtime Resolution

All gate-profile consumers (plan acceptance, patch QA-index derivation, gate execution) resolve profile via:

1. validated `state.gate_profile`
2. otherwise policy default profile
3. otherwise deterministic fallback (`default` if present, else lexical first profile)

If no profiles exist, fail with deterministic config error.

## 3.2 Canonicalization Rule for Planner Input

At `plan.submit`/`plan.update` time:

- incoming `plan_json.gate_profile` is treated as **non-authoritative input**
- canonical persisted plan uses system-resolved profile semantics
- planner-supplied `gate_profile` mismatches are logged as compatibility findings

Normative behavior:

- do not block plan solely because planner provided `gate_profile = full|fast|merge`
- preserve plan acceptance determinism by ignoring/re-writing non-authoritative profile field
- use `gate_targets` for mode intent

## 3.3 Legacy Compatibility Rule

Legacy persisted artifacts must self-heal without manual intervention.

If `state.gate_profile` is invalid:

- reconcile to policy default/fallback profile
- append deterministic note to decisions log
- continue execution without `internal_error`

If legacy `plan.gate_profile` equals a known mode token and `gate_targets` omits it:

- canonicalizer appends that mode to normalized `gate_targets`
- profile remains system-owned and valid

---

## 4. Contract and Schema Deltas

## 4.1 Policy Schema Additions

Add additive policy section:

```yaml
gate_selection:
  default_profile: default
  auto_heal_invalid_state_profile: true
  legacy_mode_profile_compat: true
```

Rules:

- `default_profile` must reference an existing key in `gates.profiles`
- if invalid, resolver falls back deterministically and emits warning finding
- `auto_heal_invalid_state_profile=true` enables state reconciliation writes

## 4.2 Plan Schema Changes (Backward Compatible)

`plan.schema.json` changes:

- remove `gate_profile` from `required`
- keep `gate_profile` as optional deprecated compatibility field
- update description: planner-supplied `gate_profile` is advisory and may be ignored
- keep `gate_targets` as optional authoritative planner-specified mode subset

## 4.3 State Schema Clarification

Update `state.schema.json` description:

- `gate_profile` is system-resolved from policy/runtime resolver, not directly copied from planner plan input

## 4.4 Gates Schema Clarification

Update `gates.schema.json` description:

- active profile is referenced from `state.gate_profile` (authoritative), not plan ownership

---

## 5. Implementation Plan

### M49.1 — Add Gate Selection Resolver

Create:

- `apps/control-plane/src/application/services/gate-selection-service.ts` (new)

Responsibilities:

- validate profile existence against gates config
- resolve effective profile deterministically from state/policy
- normalize legacy planner input into canonical plan shape
- expose structured findings for observability/tests

### M49.2 — Plan Service Canonicalization

Update:

- `apps/control-plane/src/application/services/plan-service.ts`

Changes:

- call resolver during `planSubmit/planUpdate`
- stop treating planner `gate_profile` as authoritative
- persist canonical plan shape and write/retain deterministic resolved profile in state
- emit typed warnings (log notes) for planner profile mismatches

### M49.3 — Patch Service Decoupling from Plan Profile

Update:

- `apps/control-plane/src/application/services/patch-service.ts`

Changes:

- derive profile from resolved state/profile resolver, not raw `plan.gate_profile`
- maintain deterministic `UNKNOWN_GATE_PROFILE_OR_MODE` only for unrecoverable config cases

### M49.4 — State Auto-Heal Hook

Update:

- `apps/control-plane/src/core/kernel.ts` feature init/context paths
- optionally `feature-state-service.ts` helper

Changes:

- reconcile invalid persisted `state.gate_profile` on first touch when policy allows
- write corrected profile atomically under feature lock
- append deterministic decision note

### M49.5 — Supervisor Planner Input Surface Tightening

Update:

- `apps/control-plane/src/supervisor/worker-decision-loop.ts`

Changes:

- remove `gate_profile` from planner top-level accepted keys or mark as ignored/deprecated path
- preserve `gate_targets` pass-through

### M49.6 — Prompt and Documentation Alignment

Update:

- `agentic/orchestrator/prompts/planner.system.md`
- `README.md`
- `AGENTS.md` and `CLAUDE.md` command/contract notes where relevant

Changes:

- planner no longer instructed to pick gate profile
- planner contract focuses on `gate_targets` when mode narrowing is required

---

## 6. Deterministic Migration Matrix

| Persisted State Profile | Persisted Plan Profile | Persisted Plan Targets | Effective Profile       | Effective Targets | Action          |
| ----------------------- | ---------------------- | ---------------------- | ----------------------- | ----------------- | --------------- |
| valid                   | any                    | any                    | state profile           | canonical targets | keep state      |
| invalid                 | any                    | any                    | policy default/fallback | canonical targets | auto-heal state |
| missing                 | any                    | any                    | policy default/fallback | canonical targets | auto-heal state |

Legacy plan compatibility (`legacy_mode_profile_compat=true`):

- if `plan.gate_profile` is one of `fast|full|merge` and not a profile key:
  - include that token in normalized `gate_targets` when absent
  - do not treat it as profile

---

## 7. Test Strategy (Required)

## 7.1 Unit Tests

Add/extend:

- `apps/control-plane/test/gate-selection-service.spec.ts` (new)
  - profile resolution precedence
  - invalid state profile auto-heal decisions
  - legacy mode token normalization into `gate_targets`
  - deterministic fallback selection
- `apps/control-plane/test/plan-service.spec.ts`
  - planner profile ignored/canonicalized
  - plan accepted when planner emits `gate_profile=full`
  - state profile remains valid/system-owned
- `apps/control-plane/test/patch-service.spec.ts`
  - required test derivation uses state-resolved profile
  - legacy plan with mode-token profile does not crash

## 7.2 Integration Tests

Add/extend:

- `apps/control-plane/test/supervisor.unit.spec.ts`
  - planner output containing `gate_profile=full` still produces accepted canonical plan
- `apps/control-plane/test/kernel.spec.ts`
  - feature with invalid persisted `state.gate_profile` auto-heals and proceeds
- `apps/control-plane/test/mcp.spec.ts`
  - transport parity for canonicalized plan submit + subsequent `repo.apply_patch`

## 7.3 Regression Guard

Required regression assertions:

- no `internal_error` for gate profile/mode confusion path
- error surfaces as typed config/contract error only when profiles config itself is invalid

---

## 8. Acceptance Criteria

1. Planner-provided `gate_profile` is no longer execution-critical.
2. `state.gate_profile` is the sole runtime authority for profile selection.
3. Legacy plans/states with mode-token profile values no longer crash builder/QA patch paths.
4. `repo.apply_patch` and `gates.run` use the same deterministic resolver behavior.
5. Prompts/documentation reflect system-owned profile semantics and planner-owned `gate_targets` semantics.
6. `npm run lint`, `npm run typecheck`, and `npm test` pass with >=90% thresholds.

---

## 9. Risks and Mitigations

1. Hidden behavior change for teams relying on planner profile overrides.
   - Mitigation: explicit policy-driven override strategy in follow-up spec; compatibility logging for mismatches.
2. Migration side effects on very old feature artifacts.
   - Mitigation: auto-heal under lock + idempotent writes + explicit decision log entries.
3. Divergence between resolver and direct lookups if not fully adopted.
   - Mitigation: ban direct profile lookups outside resolver via architecture validation rule.

---

## 10. Rollout and Rollback

### 10.1 Rollout

1. Merge resolver + canonicalization + tests.
2. Enable policy defaults (`gate_selection`) with conservative compatibility toggles on.
3. Run one release with compatibility logging enabled.
4. After stability, optionally tighten to reject deprecated planner profile field in a future milestone.

### 10.2 Rollback

- Keep resolver behind additive policy toggles.
- If regressions appear, disable auto-heal and legacy compat toggles while retaining crash-safe guards.

---

## 11. Definition of Done

- All M49 milestones implemented and tested.
- No open TODOs in profile resolution code paths.
- Documentation and planner prompt contract updated.
- New/updated tests prove no recurrence of `.modes` undefined failure class.

---

## 12. Spec Gap Tracker

| ID    | Area                               | Priority | Status      | Notes                                                           |
| ----- | ---------------------------------- | -------- | ----------- | --------------------------------------------------------------- |
| DGS-1 | System-owned gate profile contract | P0       | ✅ Complete | Implemented resolver + schema + service wiring                  |
| DGS-2 | Planner profile de-authoritization | P0       | ✅ Complete | Plan canonicalization + supervisor key filtering shipped        |
| DGS-3 | Legacy artifact auto-heal          | P0       | ✅ Complete | Invalid state/profile reconciliation + decision logging shipped |
| DGS-4 | Prompt/doc alignment               | P1       | ✅ Complete | Planner prompts + README contract updates completed             |
| DGS-5 | Integration parity tests           | P1       | ✅ Complete | Unit/integration parity coverage added for canonicalized flow   |
