Docs / Admin

Ops control plane and shift readiness

Summary — The ops-control module is the platform's governed engineering-loop backend: change requests, incidents, feature flags, maintenance windows, and a full feature-request lifecycle with tasks and release gates. Today, two admin routes expose this: /admin/ops (a read-only rollup dashboard) and /admin/features (the only control-plane record type with a full create-and-transition UI). Shift readiness is a separate, static code-coupling report at /admin/ops/shift-readiness.

Purpose — Understand what each ops-control record type is for, which ones have working admin UI today versus module-only code, and how the feature-request lifecycle's tasks and status gates work end to end.

Audience — Admin role for all routes below (all under the (authenticated)/admin route group). Feature-request actions require staff (create) or lead (all lifecycle transitions).

Prerequisites — Feedback reports are the primary way change requests and incidents get created today — see Feedback and friction intake.


Overview

Key concepts:

  • Change request — A governed unit of engineering work (change_requests table), created directly or via feedback linking. Has a 32-value status enum spanning intake through production release.
  • Ops incident — A live production issue (ops_incidents), 6-status lifecycle from open to closed.
  • Feature spec — A governed feature-delivery record (feature_specs), wraps a change request with a business goal, launch strategy, and a task board. This is the one record type with full CRUD-style admin UI.
  • Feature task — A unit of implementation work under a feature spec, with a recommended model (haiku/sonnet/opus) matching the project's model-routing rule.
  • Feature flag — A simple key/enabled/description toggle (feature_flags), cached in Redis. Module functions exist (isFeatureEnabled, setFeatureFlag) but there is no admin page that calls setFeatureFlag — flags are set programmatically or at the database level today.
  • Maintenance window — A start/end record (maintenance_windows) gating isMaintenanceMode(). Module functions exist; no admin UI route currently calls them.
  • Shift readiness — A static, build-time JSON report (shift-readiness-report.json) generated by npm run shift:check, showing vendor-SDK coupling and cross-module import hygiene — not a live database-backed feature.

Screenshot: Admin → Engineering Loop dashboard showing "feedback reports converted to change requests", "open incidents", and "feature flags changed this week" stat tiles above change-requests-by-phase and incidents-by-severity panels.


Engineering loop dashboard (/admin/ops)

Path: Admin → Engineering Loop (/admin/ops)

This page is read-only — it aggregates listChangeRequests(), listOpsIncidents(), listFeatureFlags(), and listFeedbackReports() into summary tiles and lists. There are no forms or action buttons on this page.

Stat tiles

TileComputation
Feedback → change requestsCount of feedback reports with a non-null linkedChangeRequestId, out of total feedback reports
Open incidentsCount of ops_incidents where status is not closed/resolved
Feature flags changed this weekCount of flags with updatedAt within the last 7 days

Change requests by phase

The 32-value ChangeRequestStatus enum is collapsed client-side by phaseOf():

  • new/classifiedintake
  • closed/rejecteddone
  • everything else → the text before the first underscore (e.g. sandbox_runningsandbox, production_passedproduction)

Displayed in a fixed phase order: intake, mitigated, sandbox, test, uat, preprod, staging, approval, canary, production, done — only phases with at least one change request are shown.

Open incidents by severity and recurring types

Two lists: open-incident counts grouped by severity (critical/high/medium/low, only non-zero shown), and open-incident counts grouped by incidentType, sorted by count descending — this is how recurring incident categories (e.g. repeated reliability incidents) surface.

Feature flags changed this week

Lists each flag changed in the last 7 days with its key, on/off state, and last-updated date. This is the only place flag activity is visible in the admin UI — there is no flag list/toggle page.

What's explicitly not here yet

The page ends with a note: "Release runs and rollback history will appear here once the release_runs control-plane table ships." This confirms release-run tracking is planned, not built.


Feature requests (/admin/features)

Path: Admin → Feature Requests (/admin/features)

This is the one ops-control record type with a complete create-and-manage UI.

Submitting a request

The "New Feature Request" collapsible form (createFeatureRequestAction, requires staff role) captures: title, summary ("What should it do?"), businessGoal ("Business goal — why does this matter?"), moduleScope, riskLevel (low/medium/high, default low), and launchStrategy (dark_launch/operator_only/limited_rollout/full_release, default full_release).

Feature lifecycle (FeatureSpecStatus)

26 possible statuses, transitions centralized in feature-lifecycle.ts per project rule 15:

requested → clarification_required → requested
requested → classified → brief_ready → task_packaged
  → sandbox_in_progress → (sandbox_failed ↺ | sandbox_passed)
  → test_in_progress → (test_failed ↺ sandbox/test | test_passed)
  → uat_in_progress → (uat_failed ↺ sandbox/uat | uat_passed)
  → preprod_in_progress → (preprod_failed ↺ sandbox/preprod | preprod_passed)
  → staging_in_progress → (staging_failed ↺ sandbox/staging | staging_passed)
  → approval_pending → release_ready
  → (canary_running → canary_failed → release_ready | rollback_requested) → canary_passed
  → released → (observing | rollback_requested)
  → observing → (closed | rollback_requested)
  → rollback_requested → rolled_back → closed

closed and rejected are terminal (no forward transitions). rejected is reachable from any non-terminal status — every stage can be rejected outright.

On the detail page (/admin/features/[id]), only legal next statuses render as buttons ("Advance lifecycle" section) — each is a one-click transition via transitionFeatureSpecAction (lead role).

Feature tasks

Each feature spec has a task board (feature_tasks). "Add task" (lead role) captures taskKey (short code like "T1"), title, recommendedModel (haiku/sonnet/opus — mirrors the CLAUDE.md model-routing rule), and an optional summary. Task statuses (FeatureTaskStatus): new, ready, in_progress, blocked, review, done, cancelled — set via a dropdown + "set" button per task, no gating between statuses (any status can move to any other from the UI).

Linked change request

Each feature spec wraps exactly one change_requests row (spec.changeRequestId), shown read-only on the detail page as "{title} ({status}) · risk {riskLevel}" — the change request's own status is not independently editable from this page.

Timeline

Below tasks, the detail page renders the feature spec's change-request events (listChangeRequestEvents) as a flat timeline — event type, optional summary, and timestamp.


Change requests and incidents — module exists, no dedicated list/detail UI

change-requests.ts and incidents.ts provide full CRUD-equivalent functions (createChangeRequest, listChangeRequests, updateChangeRequestStatus, listChangeRequestEvents; createOpsIncident, listOpsIncidents, transitionOpsIncident, listOpsIncidentEvents, canTransitionIncident, allowedIncidentTransitions), and transitionOpsIncidentAction is exported from feature-actions.tsbut no admin route currently calls it.

In practice, today:

  • Change requests and incidents are created by linking a feedback report ("Create Change Request" / "Open Incident" on /admin/feedback/[id] — see Feedback and friction intake).
  • A change request is otherwise only visible read-only, either aggregated into the /admin/ops phase counts or embedded in a feature spec's detail page.
  • Incidents are only visible aggregated into /admin/ops severity/type counts — there is no incident list or detail page, and no admin action to transition an incident's status (open → acknowledged → mitigated → monitoring → resolved → closed) even though the transition function and action both exist in code.

Incident status transitions (INCIDENT_TRANSITIONS, for reference — not yet reachable from any admin page):

FromAllowed to
openacknowledged, mitigated
acknowledgedmitigated
mitigatedmonitoring, resolved
monitoringresolved
resolvedclosed
closed— (terminal)

Feature flags — module exists, no admin UI

feature-flags.ts provides isFeatureEnabled(key) (Redis-cached, 5-minute TTL, checked by application code to gate behavior), getFeatureFlag, listFeatureFlags, setFeatureFlag (upserts and invalidates the cache, logs feature_flag.enabled/feature_flag.disabled), and deleteFeatureFlag.

listFeatureFlags() is consumed by /admin/ops for the "changed this week" tile, but no route calls setFeatureFlag or deleteFeatureFlag — there is no toggle UI. Flags are set today only via direct database access or another code path calling setFeatureFlag programmatically.


Maintenance windows — module exists, no admin UI

maintenance.ts provides isMaintenanceMode() (Redis-cached, 60s TTL), getActiveMaintenanceWindow(), startMaintenance(reason, userId, endsAt?), and endMaintenance(id, userId). No admin route references any of these functions — maintenance windows can only be started/ended by calling these functions directly (e.g., from a script or another workflow), not from /admin/ops or any other page found in the codebase.


Shift readiness (/admin/ops/shift-readiness)

Path: Admin → Engineering Loop → "Shift-readiness →" link (/admin/ops/shift-readiness)

This is not a live database query — it renders a static JSON file (shift-readiness-report.json) bundled at build time by npm run shift:check (scripts/shift-readiness-check.mjs), via getShiftReadinessReport(). The report reflects the codebase at the last time that script was run, not the current moment.

What it measures

MetricMeaning
Vendor SDK imports outside platform/Count of files importing a third-party vendor SDK directly instead of through a src/lib/platform/* adapter — the metric this project's "platform adapter" rule is meant to keep at zero
Cross-module internal importsCount of files reaching into another module's internals instead of its exported barrel (index.ts) — violates the module-boundary rule (project rule 4)
Platform adapters present / expectedCoverage ratio of expected platform adapters (AI, OCR, storage, email, cache, signatures, workflows) that actually exist

Each finding lists the file, line number, and (for vendor imports) the package name or (for cross-module imports) the offending import path. Adapter coverage renders as a green/red grid per service with a ✓/✗ indicator.

This report exists specifically to answer "how expensive would it be to swap a vendor" — a zero-finding report means the codebase is fully insulated behind its platform adapters, matching the "Zero Self-Hosted Servers" / managed-runtime philosophy in this project's stack.


Roles and permissions

ActionRequired role
View /admin/ops dashboardadmin (route group gate)
View /admin/ops/shift-readinessadmin (route group gate)
Create a feature requeststaff
List feature specsno explicit action-level check found beyond the admin route group gate
Transition a feature spec / create or update a feature tasklead
Transition an ops incident (transitionOpsIncidentAction)lead — exists in code, not wired to any page

Best practices

  1. Route bugs and incidents through feedback, not ops-control directly — since there's no standalone change-request or incident creation UI, the feedback intake flow at /admin/feedback is the only supported entry point today.
  2. Use riskLevel and launchStrategy deliberately when submitting a feature request. dark_launch/operator_only/limited_rollout exist specifically so higher-risk features don't default to full_release.
  3. Keep feature tasks small and model-tagged. The recommendedModel field exists to route implementation work to the right model size per this project's Opus/Sonnet/Haiku routing rule — tag tasks accordingly when creating them.
  4. Check shift-readiness after adding a new vendor SDK. Re-run npm run shift:check if you've added a direct SDK import; a nonzero "vendor imports outside platform" count means the platform-adapter rule was skipped.

Warnings

The /admin/ops dashboard is read-only. There are no action buttons — do not look here for a way to create or edit a change request or incident.

Feature flags cannot be toggled from any admin page. setFeatureFlag/deleteFeatureFlag exist only as module functions; changing a flag today requires calling them from code or editing the feature_flags table directly.

Maintenance windows have no admin UI at all. startMaintenance/endMaintenance are unreferenced by any route in the codebase as of this writing.

Shift-readiness is a snapshot, not live. It only reflects the state of the codebase as of the last npm run shift:check run — a clean report does not guarantee the current working tree is clean if the script hasn't been re-run since.

Incidents cannot be transitioned from any admin page, even though transitionOpsIncidentAction exists and is exported — only creation (via feedback linking) is currently reachable.


Troubleshooting

"Feature transition '{from}' → '{to}' not allowed"

Symptom: Server error when advancing a feature spec's lifecycle.

Cause: The target status isn't in FEATURE_TRANSITIONS[from] (canTransitionFeature returned false).

Fix: Only buttons for legal transitions render on the detail page — if this occurs, the spec's status likely changed since the page loaded. Reload and retry.


Feature flag change doesn't take effect immediately

Symptom: After a flag is changed at the database level, the app still behaves as if the old value were set for up to 5 minutes.

Cause: isFeatureEnabled() caches the result in Redis for 300 seconds. setFeatureFlag() calls cacheDel() to invalidate on write, but any direct database update bypasses this and leaves the stale cached value in place.

Fix: Always change flags through setFeatureFlag() (which invalidates the cache), not a raw database update. If a raw update was used, wait out the 5-minute TTL or manually clear the Redis key.


Shift-readiness numbers look stale

Symptom: The report doesn't reflect a recent change (e.g., a new vendor SDK import that should show up as a finding).

Cause: The report is a bundled static JSON file generated by npm run shift:check at build time — it does not re-scan on each page load.

Fix: Re-run npm run shift:check and redeploy/rebuild to refresh shift-readiness-report.json.


FAQ

Where do I create a new change request or incident directly, without going through feedback? There is no such admin page currently. The only reachable creation path is linking a feedback report at /admin/feedback/[id] — see Feedback and friction intake.

Can I toggle a feature flag from the admin dashboard? No. /admin/ops only shows which flags changed in the last 7 days — there is no toggle control anywhere in the admin UI.

What's the difference between a change request and a feature spec? A change request is the generic governed-work record (bug, performance, incident-driven fix, feature, or automation change) with its own 32-status pipeline. A feature spec is a richer wrapper specifically for net-new features — it owns exactly one change request plus a business goal, launch strategy, and a task board.

Does /admin/ops/shift-readiness query the database? No — it reads a static JSON file generated by a separate script (npm run shift:check), bundled at build time.

Who can create a feature request versus advance its lifecycle? Any staff-role user can submit a request. Only lead (or admin) can advance its lifecycle status or manage its tasks.


Related articles