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_requeststable), 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 fromopentoclosed. - 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 callssetFeatureFlag— flags are set programmatically or at the database level today. - Maintenance window — A start/end record (
maintenance_windows) gatingisMaintenanceMode(). Module functions exist; no admin UI route currently calls them. - Shift readiness — A static, build-time JSON report (
shift-readiness-report.json) generated bynpm 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
| Tile | Computation |
|---|---|
| Feedback → change requests | Count of feedback reports with a non-null linkedChangeRequestId, out of total feedback reports |
| Open incidents | Count of ops_incidents where status is not closed/resolved |
| Feature flags changed this week | Count 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/classified→intakeclosed/rejected→done- everything else → the text before the first underscore (e.g.
sandbox_running→sandbox,production_passed→production)
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.ts — but 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/opsphase counts or embedded in a feature spec's detail page. - Incidents are only visible aggregated into
/admin/opsseverity/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):
| From | Allowed to |
|---|---|
open | acknowledged, mitigated |
acknowledged | mitigated |
mitigated | monitoring, resolved |
monitoring | resolved |
resolved | closed |
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
| Metric | Meaning |
|---|---|
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 imports | Count 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 / expected | Coverage 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
| Action | Required role |
|---|---|
View /admin/ops dashboard | admin (route group gate) |
View /admin/ops/shift-readiness | admin (route group gate) |
| Create a feature request | staff |
| List feature specs | no explicit action-level check found beyond the admin route group gate |
| Transition a feature spec / create or update a feature task | lead |
Transition an ops incident (transitionOpsIncidentAction) | lead — exists in code, not wired to any page |
Best practices
- 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/feedbackis the only supported entry point today. - Use
riskLevelandlaunchStrategydeliberately when submitting a feature request.dark_launch/operator_only/limited_rolloutexist specifically so higher-risk features don't default tofull_release. - Keep feature tasks small and model-tagged. The
recommendedModelfield 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. - Check shift-readiness after adding a new vendor SDK. Re-run
npm run shift:checkif 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
- Feedback and friction intake — The primary path into change requests and incidents today.
- Audit log and GDPR data governance — Every change-request, incident, and feature-flag mutation is also written to
audit_events. - Roles and access — Who can do what across the platform.