Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
b8f18d6
feat(mission-control): add quick assign UI and unskip AC1 gating
Mar 3, 2026
83e71f9
chore(mission-control): update temp tracker with AC1 slice + PR
Mar 3, 2026
e9cad52
test(mission-control): unskip AC2/AC5b by using item details activity…
Mar 3, 2026
0bdeca0
feat(mission-control): wire list presence indicator and AC3 gate
Mar 3, 2026
03d2d48
test(mission-control): harden seeded auth readiness and list creation…
Mar 3, 2026
0e210f9
test(e2e): make mission-control auth readiness explicit and env-seeded
Mar 3, 2026
3dd1bae
test(mission-control): harden AC5 perf fixture seeding path
Mar 3, 2026
dc0e6a5
chore(mission-control): tighten observability routing provisioning va…
Mar 3, 2026
f0134cb
fix(mission-control): dedupe active presence and prune expired sessions
Mar 3, 2026
ff4ff6b
polish readiness drill auth split for launch-gate checks
Mar 3, 2026
7071bea
mission-control: enforce severity-based alert routing policy
Mar 3, 2026
e010ed3
test(e2e): add AC0 auth readiness probe with auto diagnostics artifacts
Mar 3, 2026
fc38ac5
mission-control: enforce severity-based alert routing policy
Mar 3, 2026
bdc69bc
test(e2e): add AC0 auth readiness probe with auto diagnostics artifacts
Mar 3, 2026
3fd2deb
Harden AC5 perf gates for CI with deterministic reporting
Mar 3, 2026
0e48dd9
Harden API key rotation flow and drill contracts
Mar 3, 2026
576a197
feat(mc): harden artifact retention idempotency and audit schema
Mar 3, 2026
ea9f785
merge: mission control AC1 assign slice (#153)
Mar 3, 2026
0be7044
merge: 5-wide integration baseline (#161)
Mar 3, 2026
857c938
merge: perf thresholds CI enforceability (#164)
Mar 3, 2026
be01757
merge: observability severity routing policy (#162)
Mar 3, 2026
93addc5
merge: auth readiness AC0 + diagnostics (#163)
Mar 3, 2026
43727de
merge: api key rotation hardening (#165)
Mar 3, 2026
299ba1f
merge: retention hardening idempotency (#166)
Mar 3, 2026
44178e8
test(mc): harden readiness drill contracts for key rotation/retention
Mar 3, 2026
646d78b
test(ci): add mission-control phase1 quality gates workflow
Mar 3, 2026
6ff3e0b
fix(memory-sync): make since cursor paging lossless
Mar 3, 2026
4c99a60
feat(observability): close out remaining run-control and heartbeat me…
Mar 3, 2026
a5cffce
feat(mission-control): add kill-path coverage to readiness drill
Mar 3, 2026
067a006
test(e2e): attach deterministic diagnostics for feature-gated skips
Mar 3, 2026
0088155
mission-control: enforce slack+pagerduty escalation coverage
Mar 3, 2026
aaa2f8c
Harden readiness drill retention contract validation
Mar 3, 2026
374fa40
feat(obs): emit run-control failure metrics for rejected and error paths
Mar 3, 2026
4589cce
test(mc): harden alert-routing readiness drill checks
Mar 3, 2026
4d03241
feat(memory-sync): Phase 3 bidirectional sync with conflict handling
Mar 3, 2026
e2990a8
feat(phase3): add validation script and npm scripts for Phase 3 memor…
Mar 3, 2026
95ce269
fix: resolve convex codegen type issues
Mar 3, 2026
a68f9fe
fix: resolve build errors
Mar 3, 2026
aa0974c
fix: resolve test failures
Mar 3, 2026
c320774
docs(mission-control): add comprehensive usage and testing HOWTO
Mar 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/mission-control-quality-gates.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: mission-control-quality-gates

on:
pull_request:
paths:
- "e2e/**"
- "playwright.config.ts"
- "package.json"
- ".github/workflows/mission-control-quality-gates.yml"
workflow_dispatch:

jobs:
phase1-quality-gates:
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5

- name: Install dependencies
run: npm ci

- name: Install Playwright browser
run: npx playwright install --with-deps chromium

- name: Perf fixture parser gate
run: npm run test:e2e -- e2e/mission-control-perf-fixture.spec.ts --reporter=line

- name: Mission Control Phase 1 acceptance + perf gates
env:
MISSION_CONTROL_FIXTURE_PATH: e2e/fixtures/mission-control.production.json
run: npm run test:e2e -- e2e/mission-control-phase1.spec.ts --reporter=line

- name: Upload Playwright artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: mission-control-playwright-artifacts
path: |
playwright-report/
test-results/
if-no-files-found: ignore
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ android/.gradle/
android/app/build/
android/build/
.secrets/
playwright-report/
test-results/
10 changes: 9 additions & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ New endpoints for Agent Mission Control with scoped API keys.
### Memory
- `GET /api/v1/memory?agentSlug=<slug>[&key=<key>]` (`memory:read`)
- `POST /api/v1/memory` (`memory:write`)
- `GET /api/v1/memory/sync?since=<ms>&limit=<n>` (`memory:read`) — pull Convex memory changes for OpenClaw
- `GET /api/v1/memory/sync?since=<ms>&limit=<n>` (`memory:read`) — pull Convex memory changes for OpenClaw (results are ordered oldest→newest after `since`; response `cursor` equals newest returned `updatedAt` for lossless paging)
- `POST /api/v1/memory/sync` (`memory:write`) — push OpenClaw memory entries into Convex with conflict policy (`lww` or `preserve_both`)
- body: `{ "agentSlug": "platform", "key": "runbook", "value": "...", "listId": "...optional..." }`

Expand All @@ -238,6 +238,14 @@ New endpoints for Agent Mission Control with scoped API keys.
- `GET /api/v1/runs/retention` (JWT only) — retention config + recent deletion logs
- `PUT /api/v1/runs/retention` (JWT only) — set artifact retention days (default 30)
- `POST /api/v1/runs/retention` (JWT only) — run retention job (`dryRun` defaults to `true`)
- retention clamp: `1..365` days, stale rule: `artifact.createdAt < cutoff`
- audit logs are idempotent on `(runId, retentionCutoffAt, dryRun, deletedArtifacts fingerprint)`

### Launch-gate drill auth split
For `npm run mission-control:readiness-drill`:
- `MISSION_CONTROL_BASE_URL` — Convex site base URL
- `MISSION_CONTROL_API_KEY` — used for API-key routes (dashboard/run controls)
- `MISSION_CONTROL_JWT` — used for JWT-only routes (API key rotation inventory + retention/audit endpoints)

### Run Dashboard
- `GET /api/v1/dashboard/runs?[windowMs=86400000]` (`dashboard:read`)
Expand Down
119 changes: 106 additions & 13 deletions MISSION-CONTROL-TEMP-TRACKER.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"sprintDate": "2026-02-23",
"block": "5/5",
"updatedAt": "2026-03-02T07:35:00Z",
"updatedAt": "2026-03-03T09:51:00Z",
"pooAppAgentApiTracking": {
"attempted": true,
"status": "blocked",
Expand Down Expand Up @@ -46,31 +46,124 @@
{
"id": "MC-P1-PR-OPEN",
"title": "Open/update PR with overnight mission control scope summary",
"status": "pending",
"artifacts": []
"status": "done",
"artifacts": [
"https://github.com/aviarytech/todo/pull/153"
]
},
{
"id": "MC-P1-AC3-PRESENCE-WIRE",
"title": "Wire list-level presence indicator + heartbeat and unskip AC3 feature gate",
"status": "done",
"artifacts": [
"src/pages/ListView.tsx",
"e2e/mission-control-phase1.spec.ts"
]
},
{
"id": "MC-P3-MEMORY-E2E",
"title": "Phase 3 Memory System E2E Tests + Validation",
"status": "done",
"artifacts": [
"e2e/mission-control-phase3-memory.spec.ts",
"scripts/validate-mission-control-phase3.mjs"
],
"notes": "Added comprehensive e2e tests covering: Memory Browser UI (search, filters, conflict banner), Memory API (CRUD, search, sync), Bidirectional Sync (cursor pagination, LWW conflict resolution), Performance gates (500ms list, 700ms search, 500ms sync)"
}
],
"validation": {
"playwrightSpecRun": "partial",
"command": "npm run test:e2e -- e2e/mission-control-phase1.spec.ts",
"command": "npm run test:e2e -- e2e/mission-control-phase1.spec.ts -g \"AC3 presence freshness\"",
"result": {
"passed": 1,
"skipped": 6,
"passed": 0,
"skipped": 1,
"failed": 0
},
"observabilityValidation": {
"command": "npm run mission-control:validate-observability",
"passed": true
},
"phase3Validation": {
"command": "npm run mission-control:validate-phase3",
"passed": true,
"checksRun": 24,
"checksPassed": 24
},
"memorySyncUnitTests": {
"command": "bun test convex/lib/memorySync.test.ts",
"passed": true,
"testsRun": 14,
"testsPassed": 14
},
"notes": [
"Seeded local auth fixture added for OTP-gated routes; baseline harness remains runnable.",
"AC1/AC2/AC3/AC5b remain conditionally skipped when assignee/activity/presence UI surfaces are absent in current build.",
"Perf harness supports production-sized fixture path via MISSION_CONTROL_FIXTURE_PATH."
"Added quick Assign action in list item UI wired to items.updateItem(assigneeDid=userDid).",
"Removed AC1 feature-availability dynamic skip; AC1 now asserts Assign control visibility.",
"Remaining AC1 skip is environment readiness gate (authenticated app shell availability).",
"AC3 feature dynamic skip removed; scenario still environment-gated on authenticated app-shell readiness.",
"Phase 3 memory system fully validated: schema + backend + API + UI + e2e tests."
]
},
"next": [
"Wire assignee/activity/presence UI+backend then remove dynamic skips",
"Run production-sized perf profile: MISSION_CONTROL_FIXTURE_PATH=e2e/fixtures/mission-control.production.json npm run test:e2e -- e2e/mission-control-phase1.spec.ts",
"Open PR with this P0-3/P0-4 delta and CI artifacts"
]
"Acquire stable authenticated e2e backend session so AC3 can execute instead of setup-skip",
"Run full mission-control-phase1 spec on production-sized fixture to capture AC5 metrics without skips",
"Close MC-P1-TRACKING-AUTH blocker once agent API credentials/session are provisioned",
"Run Phase 3 e2e tests against production with E2E_API_KEY"
],
"phase3Summary": {
"status": "complete",
"components": {
"schema": {
"status": "done",
"table": "memories",
"features": [
"Full-text search index (searchText)",
"Bidirectional sync fields (externalId, externalUpdatedAt, lastSyncedAt, syncStatus, conflictNote)",
"Source tracking (manual/openclaw/clawboot/import/api)",
"Tags support"
]
},
"backend": {
"status": "done",
"files": ["convex/memories.ts", "convex/lib/memorySync.ts"],
"features": [
"CRUD mutations (createMemory, updateMemory, deleteMemory)",
"listMemories query with search, filters",
"upsertOpenClawMemory for sync",
"listMemoryChangesSince for cursor-based sync",
"Conflict detection + LWW resolution"
]
},
"api": {
"status": "done",
"routes": [
"GET /api/v1/memory - list with search/filter",
"POST /api/v1/memory - create (agent KV)",
"GET /api/v1/memory/sync - pull changes since cursor",
"POST /api/v1/memory/sync - push changes with conflict policy",
"PATCH /api/v1/memory/:id - update",
"DELETE /api/v1/memory/:id - delete"
],
"scopes": ["memory:read", "memory:write"]
},
"ui": {
"status": "done",
"file": "src/pages/Memory.tsx",
"features": [
"Search input",
"Source filter dropdown",
"Sync status filter",
"Tag filter",
"Conflict count banner",
"Memory card display",
"Create memory form"
]
},
"tests": {
"status": "done",
"unitTests": "convex/lib/memorySync.test.ts (14 tests)",
"e2eTests": "e2e/mission-control-phase3-memory.spec.ts",
"validation": "scripts/validate-mission-control-phase3.mjs"
}
}
}
}
4 changes: 4 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[test]
# Exclude Playwright e2e tests from bun test (run with npm run test:e2e instead)
root = "."
ignore = ["./e2e/"]
32 changes: 32 additions & 0 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
* @module
*/

import type * as activity from "../activity.js";
import type * as activityHttp from "../activityHttp.js";
import type * as agentApi from "../agentApi.js";
import type * as agentTeam from "../agentTeam.js";
import type * as assignees from "../assignees.js";
import type * as assigneesHttp from "../assigneesHttp.js";
import type * as attachments from "../attachments.js";
import type * as auth from "../auth.js";
import type * as authInternal from "../authInternal.js";
Expand All @@ -18,6 +23,10 @@ import type * as categories from "../categories.js";
import type * as categoriesHttp from "../categoriesHttp.js";
import type * as comments from "../comments.js";
import type * as didCreation from "../didCreation.js";
import type * as didLogs from "../didLogs.js";
import type * as didLogsHttp from "../didLogsHttp.js";
import type * as didResources from "../didResources.js";
import type * as didResourcesHttp from "../didResourcesHttp.js";
import type * as http from "../http.js";
import type * as items from "../items.js";
import type * as itemsHttp from "../itemsHttp.js";
Expand All @@ -28,8 +37,15 @@ import type * as lib_turnkeyClient from "../lib/turnkeyClient.js";
import type * as lib_turnkeySigner from "../lib/turnkeySigner.js";
import type * as lists from "../lists.js";
import type * as listsHttp from "../listsHttp.js";
import type * as memories from "../memories.js";
import type * as memoriesHttp from "../memoriesHttp.js";
import type * as missionControl from "../missionControl.js";
import type * as missionControlApi from "../missionControlApi.js";
import type * as missionControlCore from "../missionControlCore.js";
import type * as notificationActions from "../notificationActions.js";
import type * as notifications from "../notifications.js";
import type * as presence from "../presence.js";
import type * as presenceHttp from "../presenceHttp.js";
import type * as publication from "../publication.js";
import type * as rateLimits from "../rateLimits.js";
import type * as tags from "../tags.js";
Expand All @@ -45,7 +61,12 @@ import type {
} from "convex/server";

declare const fullApi: ApiFromModules<{
activity: typeof activity;
activityHttp: typeof activityHttp;
agentApi: typeof agentApi;
agentTeam: typeof agentTeam;
assignees: typeof assignees;
assigneesHttp: typeof assigneesHttp;
attachments: typeof attachments;
auth: typeof auth;
authInternal: typeof authInternal;
Expand All @@ -55,6 +76,10 @@ declare const fullApi: ApiFromModules<{
categoriesHttp: typeof categoriesHttp;
comments: typeof comments;
didCreation: typeof didCreation;
didLogs: typeof didLogs;
didLogsHttp: typeof didLogsHttp;
didResources: typeof didResources;
didResourcesHttp: typeof didResourcesHttp;
http: typeof http;
items: typeof items;
itemsHttp: typeof itemsHttp;
Expand All @@ -65,8 +90,15 @@ declare const fullApi: ApiFromModules<{
"lib/turnkeySigner": typeof lib_turnkeySigner;
lists: typeof lists;
listsHttp: typeof listsHttp;
memories: typeof memories;
memoriesHttp: typeof memoriesHttp;
missionControl: typeof missionControl;
missionControlApi: typeof missionControlApi;
missionControlCore: typeof missionControlCore;
notificationActions: typeof notificationActions;
notifications: typeof notifications;
presence: typeof presence;
presenceHttp: typeof presenceHttp;
publication: typeof publication;
rateLimits: typeof rateLimits;
tags: typeof tags;
Expand Down
3 changes: 2 additions & 1 deletion convex/agentTeam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ export const getTeamTree = query({
}
}

const toNode = (agent: (typeof agents)[number]) => ({
type AgentNode = (typeof agents)[number] & { children: AgentNode[] };
const toNode = (agent: (typeof agents)[number]): AgentNode => ({
...agent,
children: (childrenByParent.get(agent.agentSlug) ?? [])
.sort((a, b) => b.updatedAt - a.updatedAt)
Expand Down
33 changes: 33 additions & 0 deletions convex/lib/artifactRetention.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, test } from "bun:test";
import { artifactFingerprint, clampRetentionDays, computeRetentionCutoff, isValidArtifactRef, normalizeArtifactRefs, selectStaleArtifacts, shouldInsertDeletionLog } from "./artifactRetention";

describe("artifact retention helpers", () => {
test("clamps retention day boundaries to [1, 365]", () => {
expect(clampRetentionDays(undefined, 30)).toBe(30);
expect(clampRetentionDays(0, 30)).toBe(1);
expect(clampRetentionDays(999, 30)).toBe(365);
});

test("uses strict < cutoff semantics", () => {
const cutoff = computeRetentionCutoff(1_000_000, 1);
const artifacts = [
{ type: "log" as const, ref: "old", createdAt: cutoff - 1 },
{ type: "log" as const, ref: "edge", createdAt: cutoff },
];
expect(selectStaleArtifacts(artifacts, cutoff).map((a) => a.ref)).toEqual(["old"]);
});

test("normalizes artifact schema", () => {
expect(isValidArtifactRef({ type: "log", ref: "ok", createdAt: 1 })).toBe(true);
expect(normalizeArtifactRefs([{ type: "log", ref: "ok", createdAt: 1 }, { type: "oops", ref: "no", createdAt: 2 }])).toEqual([
{ type: "log", ref: "ok", createdAt: 1 },
]);
});

test("fingerprint supports idempotency checks", () => {
const a = [{ type: "log" as const, ref: "1", createdAt: 1 }, { type: "file" as const, ref: "2", createdAt: 2 }];
const b = [...a].reverse();
expect(artifactFingerprint(a)).toBe(artifactFingerprint(b));
expect(shouldInsertDeletionLog(a, b)).toBe(false);
});
});
Loading