name: build-ai-ops-command-center description: End-to-end recipe for building an "AI Ops Command Center" on top of any work-tracker (Basecamp, Linear, Jira, Asana, ClickUp) — a single-pane queue that mirrors active tasks, scores them deterministically, surfaces a per-task Claude Code prompt + structured suggestion, lets the operator decide inline with write-back to the source system, supports a "Run My Day" sequenced walk, captures skills, runs brand compliance preflight, and fires deterministic automation rules. Invoke when a client / project owner says "I want one place to triage everything assigned to me across all my projects, with the AI telling me what to do next on each one." user-invocable: true
Build an AI Ops Command Center — Portable Recipe
This is the recipe Colaberry shipped over 24 hours for their CEO's queue across 50 Basecamp projects. It went from "no admin/ops surface" to a production-deployed Command Center with 5,657 todos scored, structured per-task suggestions, inline decide-and-write-back, Run My Day sequenced mode, skill capture, brand compliance preflight, and a 3-rule automation engine — without a single LLM API call. Every surface is deterministic by design so it can run cheaply and predictably.
When to invoke
Invoke this skill when the user describes any variant of:
- "I want one place to see everything assigned to me across all my projects"
- "The work-tracker is fine but I waste 90 min every morning deciding what's urgent"
- "We need an exec triage page on top of Basecamp / Linear / Jira / Asana"
- "I want AI to tell me what to do next on each ticket, with the prompt ready to fire"
Do NOT invoke for:
- Simple project-dashboard requests (use a BI tool)
- Building a new work tracker from scratch (that's a different scope)
- Anything requiring autonomous outbound action without human-in-loop (the entire design philosophy here is human-decides, agent-executes-after-approval)
Core doctrine — do not skip
These six principles ship before any code. If any are violated, the system loses operator trust within a week.
- LLMs are probabilistic. The Command Center is deterministic. Rule-based scoring, rule-based compliance, rule-based automation. LLMs come ONLY in Phase 2+ as additive scorers running alongside the deterministic ones, never replacing them.
- Human decides, agent executes. The Command Center proposes; the operator approves; only then do outbound actions happen. The Approval Workspace is THE central UX, not an afterthought.
- Every operator action is auditable. Every decision lands a row in
ops_approval_queue. Every score change lands a row inops_ai_assessmentswith full breakdown. Replay is always possible. - The local mirror is read-only against the source. We sync the work-tracker IN. We never mutate upstream tickets without explicit operator decision routed through the approval flow. (Decisions DO write back as comments — that's the one explicit channel.)
- Idempotency is non-negotiable. Sync runs every 2 minutes. The same row arriving twice must converge, not duplicate. Use
INSERT ... ON CONFLICT DO UPDATEor model-level upserts. - Freshness filter is mandatory. Show only what was touched in the last N days (default 90). Ancient zombies blow up scoring + crush operator trust. The first time the operator sees a 2018 ticket scoring 80, the page loses credibility.
The 6-layer stack
┌─────────────────────────────────────────────────────────────────┐
│ 6. Presentation: /admin/ops React surface │
│ Queue tabs · Project tree · Workspace · Run My Day · Tiles │
├─────────────────────────────────────────────────────────────────┤
│ 5. Workflow: approval write-back · skill capture · prompt gen │
├─────────────────────────────────────────────────────────────────┤
│ 4. Intelligence: rule-based priority engine · brand compliance │
│ automation rules engine │
├─────────────────────────────────────────────────────────────────┤
│ 3. Agents (Phase 2+): LLM scorer · meeting eliminator · etc. │
├─────────────────────────────────────────────────────────────────┤
│ 2. Sync: poll source tracker every 2 min · upsert to mirror │
├─────────────────────────────────────────────────────────────────┤
│ 1. Data: ops_* mirror tables in primary RDBMS │
└─────────────────────────────────────────────────────────────────┘
The 6-table minimum schema (ships in Phase 0)
Field names in this schema map directly to what the Colaberry build uses. The four ops_bc_* names assume Basecamp; rename per source.
| Table | Purpose | Primary key | Key fields |
|---|---|---|---|
ops_<source>_projects |
Project mirror + per-project knobs | bc_id (source's id) |
name, is_cb_managed BOOLEAN, weight DECIMAL(3,2) DEFAULT 1.0, last_synced_at |
ops_<source>_todos |
Task mirror enriched with scores | bc_id |
project_id, todolist_id, todolist_name, title, description, status, due_on, assignee_ids JSONB, bc_app_url, urgency_score, ai_opportunity_score, brand_score, category, is_dismissed BOOLEAN, bc_created_at, bc_updated_at, last_synced_at |
ops_ai_assessments |
Score audit trail | UUID | todo_bc_id, agent (e.g. priority_v1), agent_version, urgency_score, category, reasoning JSONB (full breakdown), llm_model, llm_cost_usd, computed_at |
ops_approval_queue |
Decision audit trail | UUID | todo_bc_id, summary, recommended_decision, confidence, urgency_snapshot, enqueued_at, decided_at, decision (approve / approve_and_continue / approve_and_convert_to_skill / revise / reject / escalate), decided_by, decision_reasoning, next_actions JSONB |
ops_metrics_daily |
Pre-aggregated dashboard rollup | date PRIMARY KEY |
approvals_completed, approvals_avg_seconds, approvals_open_at_end, hours_saved_estimated, meetings_eliminated, skills_created, agent_total_cost_usd |
ops_skills |
Captured patterns from Approve+skill | UUID | name, action_kind (reply / decision / meeting / research / default), captured_from_todo_bc_id, reasoning, decision, is_active, use_count, created_by |
Phase 4+ adds ops_automation_rules (id, name, description, condition_jsonb, action_jsonb, is_active, last_fired_at, fire_count).
Critical lesson from the Colaberry build: sequelize.sync({ alter: true }) may not reach your new tables on a mature prod DB because some unrelated pre-existing index conflict short-circuits the sync. Ship CREATE TABLE IF NOT EXISTS explicit DDL that runs BEFORE the sync. The lead-ingestion tables in this repo had the same problem; we use the same pattern.
The 7-phase build order
Each phase is its own deploy. Each ends with tsc clean + git commit + push + monitor backend boot + verify endpoints + email the operator with what shipped. Do not bundle phases — granular rollback is the safety net.
Phase 0 — Foundation (1-2 days)
Goal: tables exist, sync poller works, endpoint surface authenticates, page is reachable but empty.
Build:
- 4 core models (
OpsBcTodo,OpsAiAssessment,OpsApprovalQueueItem,OpsMetricsDaily) - Explicit
ensureOpsCommandCenterSchema()withCREATE TABLE IF NOT EXISTSDDL running BEFOREsequelize.sync() bcSyncService.runBcSync()paginated: pull projects → todolists → todos, upsert bybc_id. Idempotent.- Cron
*/2 * * * *invoking the sync - Admin route module mounted at
/api/admin/ops/*gated byrequireAdmin:GET /health—{ status, todos_mirrored, open_approvals, last_sync }GET /todos,GET /metrics/today,POST /sync
- Frontend page at
/admin/opswith KPI tile row + Waiting on Human queue + System Health panel + manual Re-sync button - Source-tracker auth: a single env var (e.g.
BASECAMP_ACCESS_TOKEN). Do NOT commit tokens to repo.
Exit criterion: backend /health 200, /api/admin/ops/health 401 (gate working), all mirrored todos visible by source title in the queue.
Pitfall we hit: prod backend OOM'd at 512MB heap when the priority engine (next phase) loaded 374 Sequelize model instances + did per-row Model.update() + Model.create(). FIX: raw sequelize.query(SELECT) for read, raw UPDATE per row, bulkCreate(chunks of 100, { validate: false }) for audit-row inserts. On a mature prod heap, never use Model.findAll() + per-row Model.update() over more than ~50 rows.
Phase 1 — Priority Engine v0 (deterministic) (1-2 days)
Goal: every active todo gets urgency_score 0-100 + category written every sync.
Scorer (5 inputs, no LLM):
- Due-date proximity (40pt max): overdue=40, today=35, +1d=28, +3d=20, +7d=12, +14d=6, else=0
- Staleness since last update (20pt): >14d=20, 7-14d=12, 3-7d=6, else=0
- Title/desc keyword tier (15pt): URGENT/ASAP/CRITICAL +15; HOT/PRIORITY/P0 +8; REVIEW/APPROVE/DECIDE +5
- Assignee presence (15pt): assigned=15, orphaned=0
- Per-project signal (10pt): default flat 5; replaced in Phase 1.4b by project weight
Sum capped at 100. Category derives: human_required if ≥60 + assigned, waiting_dependency if no due + >7d stale, else unscored. Categories ai_can_finish / ai_can_prepare / can_eliminate reserved for Phase 2.
Chain runPriorityEngine() after runBcSync() in the same cron handler. Write ops_ai_assessments row per pass with reasoning: { breakdown, signals, raw_score, project_weight, weighted_score }.
Frontend: score badges (red ≥70 / amber ≥40 / gray below) + category chips per queue item + Triage Breakdown tile in System Health drawer.
Phase 1.1 — Scope-narrow + structured suggestion (2-3 days)
This is where the page becomes useful, not just busy.
- Add
ops_<source>_projectstable +is_cb_managed BOOLEAN DEFAULT TRUEflag +weight DECIMAL(3,2) DEFAULT 1.0 - Sync worker captures project name + description; sync worker passes
todolist_namedown to the todo upsert (denormalized for fast UI) - Add
STALE_HIDE_DAYS = 90constant (env-overridable). Apply to/projectsopen counts +/my-queue+ Run My Day queries. GET /api/admin/ops/projectsreturns CB-managed projects with per-project assignee-open + assignee-red counts; drives a project tab navGET /api/admin/ops/my-queue[?project_id=X]returns the operator's active assigned todos in CB-managed projects, grouped Project → Todolist → Task, sorted by urgency within each todolist- Critical discovery during build: the BC token's JWT payload
user_ids:[X]is the BOT account, NOT the operator. Look up the operator's actual id via/people.jsonfiltering by email. We hardcoded45321751(CB System bot) for the first deploy and saw zero todos in Ali's queue. The real Ali at17454835(ali@colaberry.com / Managing Director) had 293. - New service
runMyDayPromptServiceexportsbuildSuggestion(todo) → { action_kind, one_line, steps, resources: [{kind: tool|skill|agent|workflow|mcp, name, why}], stop_conditions, urgency_summary }andgeneratePrompt(todo) → full Claude Code prompt string. Action recipes are keyed on title/desc regex (reply / decision / meeting / research / default). GET /api/admin/ops/todos/:bc_id/workspace— single round-trip bundle:{ todo, suggestion, prompt, comments, decisions }with a 5-second hard timeout on the upstream comments fetch viaPromise.race. Slow upstream ships empty comments +comments_errorstring instead of hanging.
Frontend completely refactors to a project-tab nav at top + Project → Todolist → Task tree + per-task "Open workspace" button. The workspace renders structured suggestion as primary content (action-kind badge + one-line summary + numbered steps + tool/skill/agent/workflow cards + amber stop-conditions list).
/my-queue payload must be SLIM — do NOT embed prompt bodies. ~225 tasks × ~3KB prompts = 700KB JSON = the page appears stuck loading. The Colaberry build hit this exact bug; the fix was to add a has_suggestion boolean to each task and fetch the full bundle only when the operator clicks Open Workspace.
Phase 1.2 — Approval Workspace + write-back (2-3 days)
The Decide button next to Open Workspace expands an inline two-column workspace:
Left column: last 6 source-tracker comments stripped to plaintext (scrollable) + decision history block when present.
Right column: reasoning textarea + "Post back to source" checkbox + 6-button decision grid (Approve / Approve+next / Approve+skill / Revise / Reject / Escalate).
Backend recordDecision(input) does three things atomically (in this order):
- Insert a row to
ops_approval_queue(audit trail withdecided_at,decision,decided_by,decision_reasoning,urgency_snapshot). - If
post_to_bc !== false: build a color-coded HTML decision card + POST to the source comment endpoint (/buckets/<project>/recordings/<todo>/comments.jsonfor Basecamp). Wrap in try/catch — the audit row stays even if upstream errors. - Return
{ queue_item_id, bc_comment_url, bc_post_error, compliance_warnings }.
Approve + next auto-collapses current workspace + opens next task's workspace + smooth-scrolls to it. Designed for sweeping 20 decisions in 10 minutes.
Header gains rolling "Decisions today" tile pulling from GET /api/admin/ops/decisions/today?mine=true.
Phase 1.3 — Run My Day + metrics rollup (1-2 days)
Run My Day: GET /api/admin/ops/run-my-day?limit=5 returns the operator's top 5 highest-urgency todos NOT already decided today (NOT EXISTS against ops_approval_queue filtered by decided_at >= date_trunc('day', NOW()) + decided_by = req.admin.email) with their full workspace bundles pre-loaded. Green "Run My Day" button at the top of the header opens a focused panel above the project tab nav, lists the 5 tasks stacked, all workspaces auto-expanded, Approve+next auto-advances + smooth-scrolls. Reload top 5 + Exit Run My Day controls.
Metrics rollup: metricsDailyService.rollupToday() aggregates ops_approval_queue for today into ops_metrics_daily: approvals_completed (count), approvals_avg_seconds (avg of decided_at - enqueued_at), approvals_open_at_end, hours_saved_estimated (count × 0.25h conservative). Idempotent upsert. 5-minute cron. Today's Pulse tile in the System Health drawer reads from /metrics/today.
Phase 1.4 — Polish + freshness + auto-detect (1-2 days)
- 1.4a Hide-decided toggle: header checkbox, client-side filter on queue + Run My Day
- 1.4b Per-project weight (0.0-2.0): number input per project in System Health drawer; priority engine multiplies raw urgency by weight before deriving category. Lets operator down-weight noisy admin projects (set to 0.4) without losing them
- 1.4c Stale review tab: new
is_dismissed BOOLEANflag +dismissed_at/by/reasononops_<source>_todos.GET /api/admin/ops/stale-todosreturns >90d-no-activity set sorted oldest-first.POST /api/admin/ops/todos/dismissbulk +undismiss: truereverses. Dedicated view-mode tab with bulk-select + Dismiss button. Reversible. Does NOT touch upstream. - 1.4d Auto-detect CB-managed: after each sync,
UPDATE projects SET is_cb_managed = (project has ANY todo with bc_updated_at >= NOW() - INTERVAL 'OPS_CB_DORMANT_DAYS days'). Self-heals when a project revives. Replaces a manual double-click-to-dim hack.
Phase 2-light — Skill extraction (1 day)
ops_skills table. In recordDecision, when decision === 'approve_and_convert_to_skill', call buildSuggestion(todo) to get the action_kind, then OpsSkill.create({ name, action_kind, captured_from_todo_bc_id, reasoning, decision, is_active: true, use_count: 0, created_by }). Non-fatal: catch + console.warn so a skill-capture failure doesn't break the decision flow.
New view-mode tab "Captured skills" with action-kind filter + enable/disable toggle + delete. GET /api/admin/ops/skills?include_inactive=true, POST /skills/:id/toggle, DELETE /skills/:id.
Phase 3-light — Brand compliance preflight (0.5 day)
New brandComplianceService.checkCompliance(html, reasoning) → { ok, blockers, warnings }. Pure regex.
Blockers (HARD stop) — secret-leak patterns:
- Basecamp access token (
BAhbB0kiAbB7[\w+/=]{200,}) - Mandrill API key (
md-[\w-]{20,}) - Bearer header (
Bearer\s+[\w.\-]{40,}) - Google OAuth refresh (
1/\/[\w-]{60,}) - JWT-shaped (
eyJ[a-zA-Z0-9_-]{15,}\.[a-zA-Z0-9_-]{20,}\.[a-zA-Z0-9_-]{20,}) - AWS access key (
AKIA[0-9A-Z]{16})
Warnings (non-blocking) — style flags: em-dash ([—–]), "I hope this email finds you well", "just checking in", "leverage synergies", "circle back", "going forward", "low-hanging fruit".
Wires into approvalService.recordDecision BEFORE the upstream comment POST. Blockers → bc_post_error = "Brand compliance blocked: ..." and the comment is NOT posted (audit row still saved). Warnings → compliance_warnings: string[] in the response, surfaced in UI but the post still goes through.
Phase 4-light — Automation rules (1 day)
ops_automation_rules table + automationRulesService.
Rule shape: { condition_jsonb, action_jsonb }. v0 condition language: match: 'stale_days_gt' | 'urgency_gte' | 'category_eq', optional combiners. v0 action language: do: 'flag_for_archive' | 'tag_category' | 'noop_for_metrics'.
Three seeded rules (idempotent INSERT ... WHERE NOT EXISTS):
- "Flag for archive — no source activity > 180d" →
flag_for_archive - "Alert — red urgency stale > 14d" →
noop_for_metrics(counts only) - "Tag waiting_dependency — stale > 7d, no due" →
tag_category: 'waiting_dependency'
Executor chains after priority engine on the 2-min cron. UI tab with toggle + Run-now + last-run summary showing fires per rule.
Polish phase (final)
Keyboard shortcuts in Run My Day: A=approve+next, S=approve+skill, R=revise, X=reject, E=escalate. Implementation note: route through useRef so the keyboard useEffect has stable deps and doesn't need // eslint-disable-line react-hooks/exhaustive-deps — that comment breaks production builds when the project's eslint config doesn't have the react-hooks plugin loaded.
Adapting to a different source tracker
The Colaberry build is Basecamp-specific. Mapping table:
| Concept | Basecamp | Linear | Jira | Asana | ClickUp |
|---|---|---|---|---|---|
| Project | Project |
Team or Project |
Project |
Project |
Space or List |
| Container | Todolist |
Cycle or Project |
Sprint or Epic |
Section |
List |
| Task | Todo |
Issue |
Issue |
Task |
Task |
| Sync pull | /projects.json → /todoset.json → /todolists.json → /todos.json |
GraphQL issues(filter: { team: ... }) |
/rest/api/3/search?jql=... |
/projects/{gid}/tasks |
/team/{team_id}/space → list → tasks |
| Comment write-back | POST /buckets/<project>/recordings/<todo>/comments.json |
GraphQL commentCreate |
POST /rest/api/3/issue/{key}/comment |
POST /tasks/{gid}/stories |
POST /task/{task_id}/comment |
| User ID lookup | /people.json |
viewer query |
/myself |
/users/me |
/user |
| Tasks assigned to me filter | assignee_ids @> [my_id] |
assignee: { id: { eq: my_id } } |
JQL assignee = currentUser() |
assignee: me |
assignees: [my_id] |
Rename ops_bc_* tables to ops_<source>_* consistently. The bc_id primary key stays generic-named or becomes source_id.
JWT vs OAuth: Basecamp uses a long-lived rotating OAuth token. Linear / Jira / Asana / ClickUp use API keys. Either way, single env var, never in repo.
Comment fetch latency varies wildly: keep the 5-second Promise.race timeout on the comments fetch in the workspace endpoint regardless of source. Slow upstream must not hang the workspace render.
Honest deferrals (NEVER ship these in Phase 0-4-light)
These belong in Phase 2+ once the operator trusts the v0 surface:
- LLM-based scoring. The AI Opportunity Agent that scores "could AI finish this?" needs prompt engineering + a cost ceiling + a hard-fallback to the deterministic scorer when LLM is unavailable. Don't ship without all three.
- Autonomous outbound from a decision. Approval is one thing; the agent actually firing the resulting email or BC comment without operator sign-off is another. The Approval Workspace's Approve button records the decision. A separate "Execute" step (Phase 2) actually fires.
- Auto-archive on source. The local
is_dismissedflag does the right thing without ever touching the upstream ticket. If the operator says "actually mass-archive in BC," that's a separate explicit feature with confirmation. - Cross-operator queues. Phase 1 builds the operator's personal queue. Phase 2 adds shared queues + delegation + handoff. Don't bake assumptions about multi-user into Phase 1.
Tools / Skills / Agents / Workflows this skill leans on
| Kind | Name | Why |
|---|---|---|
| Tool | sendWithBcAttach (or equivalent wrapper per source) |
Auto-attaches every email this skill sends + every produced artifact to the originating ticket per the auto-attach doctrine |
| Tool | cb-context-walker.js |
Pulls full ticket context including comments + linked Vault docs + attached emails so the structured suggestion has real content to act on. The walker takes a BC URL and emits LLM-readable concatenated context. |
| Skill | screenshot-review |
The capture-helpers + Playwright walk used to ship the per-phase walkthrough HTML doc + final email screenshots |
| Skill | telemetry-emission |
After each phase deploy, emit a BuildManifest so the portal can rebuild its state map |
| Workflow | Per-phase deploy loop | tsc clean → commit → push → ssh prod → git pull → docker compose up -d --build → Monitor backend boot → curl new endpoints (expect 401) → email operator with what shipped |
Build operating cadence (proven on Colaberry)
Phase 0 → 1 → 1.1 → 1.2 → 1.3 took 10 days end-to-end with 3-4 hours of focused build per phase. Phase 1.4 + 2-light + 3-light + 4-light + polish bundled into one overnight autonomous run (6 hours). The granular per-phase commits made every step independently reversible.
Always end a phase with: deployed to prod + email to operator stating "X is live, here are the verified counts, here's what's next, here's what's deferred." Operator-trust compounds; surprises kill it.
How to invoke this skill
When a new client engagement starts that fits the "When to invoke" criteria above, paste this skill into a fresh repo's .claude/skills/build-ai-ops-command-center/SKILL.md and tell Claude Code: /build-ai-ops-command-center. The skill expects:
- A primary RDBMS (Postgres tested; SQLite would need DDL tweaks)
- A backend stack with an admin auth middleware (
requireAdminor equivalent JWT gate) - A frontend stack that can render a React tree (the queue + workspace are React-specific; an equivalent Vue / Svelte port is straightforward)
- Source-tracker API access (single env var)
- The operator's actual user id IN the source tracker (look up via people endpoint — do NOT trust the API token's payload)
Each phase ends with: ship + deploy + verify + email + commit. The skill explicitly does NOT proceed phase-to-phase silently — operator approval gates between phases except in an explicit "complete overnight" run authorized by the operator. The overnight run still defers anything LLM-driven or destructive.
Reference build
The original Colaberry build sits at:
- Repo:
ColaberryIntern/ColaberryEnterprise_AI_LeadershipAccelerator - Architecture brief:
docs/ai-ops-command-center-architecture-2026-06-02.html - Plan doc:
docs/ai-ops-overnight-plan-2026-06-02.md - Walkthrough:
docs/ai-ops-overnight-walkthrough-2026-06-02.html - Live:
enterprise.colaberry.ai/admin/ops - BC ticket: 9953889114 (AI_ProjectArchitect Overview)
- 11 phase emails on that ticket — read them in order to see how each phase landed + how the operator-feedback loop tightened the build
Session that built it: CC-20260602-9q4r over ~24h on 2026-06-02 → 2026-06-03.