@swarmclawai/swarmclaw 1.9.7 → 1.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -399,6 +399,24 @@ Operational docs: https://swarmclaw.ai/docs/observability
399
399
 
400
400
  ## Releases
401
401
 
402
+ ### v1.9.9 Highlights
403
+
404
+ Schedule revision timeline release: schedule edits, lifecycle changes, and run evidence now stay inspectable from UI, API, and CLI surfaces.
405
+
406
+ - **Schedule history ledger.** Schedules now carry a bounded revision history for create, update, archive, restore, skipped, failed, and run-started events.
407
+ - **History console.** The Schedule Console adds a searchable History tab with revision badges, actor labels, and before/after change summaries.
408
+ - **API and CLI access.** `GET /api/schedules/:id/history` and `swarmclaw schedules history <id>` expose the same timeline for scripts and operator audits.
409
+ - **Runtime evidence.** Manual runs and scheduler-fired runs append history entries, while storage normalization caps old entries and keeps legacy schedules compatible.
410
+
411
+ ### v1.9.8 Highlights
412
+
413
+ Bundled release-readiness release: a single operator report that combines eval gates, operations blockers, approvals, and runtime readiness.
414
+
415
+ - **Release readiness report.** `/api/quality/release-readiness` returns a scored ready/warning/blocked report built from eval regression gates and Operations Pulse evidence.
416
+ - **Quality Center ship gate.** The Quality overview now shows readiness score, blockers, warnings, checks, and next actions before operators cut a release.
417
+ - **CLI readiness checks.** `swarmclaw operations readiness` exposes the same report for scripts and CI.
418
+ - **Browser coverage.** The e2e smoke now verifies the release-readiness panel on `/quality`.
419
+
402
420
  ### v1.9.7 Highlights
403
421
 
404
422
  Bundled eval-gate release: approved baselines, regression checks, and Quality Center release gates for repeatable eval evidence.
@@ -694,6 +712,62 @@ Older releases: https://swarmclaw.ai/docs/release-notes
694
712
  - npm package: https://www.npmjs.com/package/@swarmclawai/swarmclaw
695
713
  - Historical release notes: https://swarmclaw.ai/docs/release-notes
696
714
 
715
+
716
+ ## FAQ
717
+
718
+ ### General
719
+
720
+ **What is SwarmClaw?**
721
+ SwarmClaw is an open-source, self-hosted AI agent runtime and multi-agent framework. It lets you run autonomous AI agents, agent swarms, and orchestrators with durable memory, MCP tools, skills, delegation, schedules, and support for 23+ LLM providers — serving as a practical alternative to Claude Code and LangChain for self-hosted workflows.
722
+
723
+ **How does SwarmClaw differ from LangChain or CrewAI?**
724
+ SwarmClaw is a self-hosted runtime rather than a code library. It provides a persistent dashboard, durable agent memory, real-time org chart visualization, and built-in multi-agent orchestration with delegation — all running on your own infrastructure. LangChain and CrewAI are code frameworks you embed in your applications; SwarmClaw is the platform your agents run on.
725
+
726
+ **Is SwarmClaw production-ready?**
727
+ Yes. SwarmClaw is used in production by teams running autonomous agent swarms. It includes security features like approval-gated actions, TLS support, and access key management.
728
+
729
+ ### Setup & Configuration
730
+
731
+ **How do I install SwarmClaw?**
732
+ Install via npm: `npm install -g @swarmclawai/swarmclaw`, then run `swarmclaw init` to create your configuration. Docker deployment is also available via the provided Docker Compose setup.
733
+
734
+ **Which LLM providers are supported?**
735
+ SwarmClaw supports 23+ providers including OpenAI, Anthropic (Claude), Google Gemini, OpenRouter, Ollama (local), DeepSeek, Groq, Together AI, and more. Configure your API keys in the `.env` file or via the dashboard.
736
+
737
+ **Can I use local models?**
738
+ Yes. SwarmClaw integrates with Ollama and other local LLM backends for fully offline agent operation.
739
+
740
+ ### Agent Development
741
+
742
+ **What is an agent swarm?**
743
+ An agent swarm is a group of AI agents working together under an orchestrator. Each agent has its own role, tools, and memory. The orchestrator delegates tasks, agents execute in parallel, and results are aggregated — mimicking a real organizational structure.
744
+
745
+ **What are MCP tools?**
746
+ MCP (Model Context Protocol) tools let agents interact with external systems — databases, APIs, file systems, browsers, and more. SwarmClaw provides a marketplace of pre-built MCP tools at SwarmDock.
747
+
748
+ **How does agent memory work?**
749
+ SwarmClaw provides durable agent memory that persists across sessions. Each agent maintains its own conversation history, learned skills, and context — enabling long-running autonomous workflows.
750
+
751
+ ### Deployment
752
+
753
+ **How do I deploy SwarmClaw?**
754
+ Options include: local npm installation, Docker Compose for containerized deployment, or cloud VPS with reverse proxy and TLS. See the [deployment docs](https://swarmclaw.ai/docs/deployment) for detailed guides.
755
+
756
+ **Can I run SwarmClaw in the cloud?**
757
+ Yes. SwarmClaw runs on any Linux server with Node.js 18+. Popular options include AWS EC2, DigitalOcean, Hetzner, and self-hosted on home servers.
758
+
759
+ ### Troubleshooting
760
+
761
+ **Agent is not responding. What should I check?**
762
+ Verify your LLM API key is valid, check the agent's configuration in the dashboard, and review the agent chat logs for error messages. Common issues include rate limiting and invalid model names.
763
+
764
+ **How do I update SwarmClaw?**
765
+ Run `npm update -g @swarmclawai/swarmclaw` or pull the latest Docker image. Check the [release notes](https://swarmclaw.ai/docs/release-notes) for breaking changes.
766
+
767
+ **Where can I get help?**
768
+ - Documentation: https://swarmclaw.ai/docs
769
+ - Discord community: https://discord.gg/sbEavS8cPV
770
+ - GitHub Issues: https://github.com/swarmclawai/swarmclaw/issues
697
771
  ## Security Notes
698
772
 
699
773
  - First run creates an access key; keep it private.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.9.7",
3
+ "version": "1.9.9",
4
4
  "description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
5
5
  "main": "electron-dist/main.js",
6
6
  "license": "MIT",
@@ -87,7 +87,7 @@
87
87
  "test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/ensure-sandbox-browser-image.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
88
88
  "test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
89
89
  "test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/gateways/gateway-topology.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/gateways/topology-route.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
90
- "test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/tts/route.test.ts",
90
+ "test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/quality/release-readiness.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
91
91
  "test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
92
92
  "test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
93
93
  "test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
@@ -0,0 +1,38 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { evaluateEvalGate } from '@/lib/server/eval/baseline'
3
+ import { getOperationPulse, normalizeOperationPulseRange } from '@/lib/server/operations/operation-pulse'
4
+ import { buildReleaseReadinessReport } from '@/lib/quality/release-readiness'
5
+ import { errorMessage } from '@/lib/shared-utils'
6
+
7
+ export const dynamic = 'force-dynamic'
8
+
9
+ function parseNumberParam(value: string | null): number | null {
10
+ if (value == null || value.trim() === '') return null
11
+ const parsed = Number(value)
12
+ return Number.isFinite(parsed) ? parsed : null
13
+ }
14
+
15
+ export async function GET(req: Request) {
16
+ try {
17
+ const { searchParams } = new URL(req.url)
18
+ const range = normalizeOperationPulseRange(searchParams.get('range'))
19
+ const agentId = searchParams.get('agentId') || ''
20
+ const pulse = getOperationPulse(range)
21
+ const evalGate = agentId
22
+ ? evaluateEvalGate({
23
+ agentId,
24
+ scenarioId: searchParams.get('scenarioId'),
25
+ suite: searchParams.get('suite'),
26
+ minPercent: parseNumberParam(searchParams.get('minPercent')),
27
+ maxRegressionPoints: parseNumberParam(searchParams.get('maxRegressionPoints')),
28
+ })
29
+ : null
30
+
31
+ return NextResponse.json(buildReleaseReadinessReport({ pulse, evalGate }))
32
+ } catch (err: unknown) {
33
+ return NextResponse.json(
34
+ { error: errorMessage(err) },
35
+ { status: 500 },
36
+ )
37
+ }
38
+ }
@@ -0,0 +1,15 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { notFound } from '@/lib/server/collection-helpers'
3
+ import { loadSchedule } from '@/lib/server/schedules/schedule-repository'
4
+ import { normalizeScheduleHistory } from '@/lib/server/schedules/schedule-history'
5
+
6
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
7
+ const { id } = await params
8
+ const schedule = loadSchedule(id)
9
+ if (!schedule) return notFound()
10
+ return NextResponse.json({
11
+ scheduleId: schedule.id,
12
+ revision: schedule.revision || 0,
13
+ history: normalizeScheduleHistory(schedule.history),
14
+ })
15
+ }
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict'
2
2
  import test, { afterEach } from 'node:test'
3
3
 
4
4
  import { DELETE as deleteScheduleRoute, PUT as updateSchedule } from './route'
5
+ import { GET as getScheduleHistory } from './history/route'
5
6
  import { loadAgents, loadSchedules, saveAgents, saveSchedules } from '@/lib/server/storage'
6
7
 
7
8
  const originalAgents = loadAgents()
@@ -84,7 +85,7 @@ test('PUT /api/schedules/[id] pauses equivalent reminder schedules together', as
84
85
  assert.equal(schedules.two.status, 'paused')
85
86
  })
86
87
 
87
- test('DELETE /api/schedules/[id] deletes equivalent reminder schedules together', async () => {
88
+ test('DELETE /api/schedules/[id] archives equivalent reminder schedules together', async () => {
88
89
  seedAgent('schedule-route-agent-delete')
89
90
  const now = Date.now()
90
91
  saveSchedules({
@@ -123,6 +124,54 @@ test('DELETE /api/schedules/[id] deletes equivalent reminder schedules together'
123
124
 
124
125
  assert.equal(response.status, 200)
125
126
  const payload = await response.json() as Record<string, unknown>
126
- assert.deepEqual(new Set(payload.deletedIds as string[]), new Set(['one', 'two']))
127
- assert.deepEqual(loadSchedules(), {})
127
+ assert.deepEqual(new Set(payload.archivedIds as string[]), new Set(['one', 'two']))
128
+ const schedules = loadSchedules()
129
+ assert.equal(schedules.one.status, 'archived')
130
+ assert.equal(schedules.two.status, 'archived')
131
+ assert.equal(schedules.one.history?.[0]?.action, 'archived')
132
+ })
133
+
134
+ test('GET /api/schedules/[id]/history returns revision history', async () => {
135
+ const now = Date.now()
136
+ saveSchedules({
137
+ one: {
138
+ id: 'one',
139
+ name: 'History Schedule',
140
+ agentId: 'schedule-route-agent-history',
141
+ taskPrompt: 'Report changes',
142
+ scheduleType: 'interval',
143
+ intervalMs: 86_400_000,
144
+ status: 'active',
145
+ revision: 2,
146
+ history: [{
147
+ id: 'history-2',
148
+ at: now,
149
+ actor: 'user',
150
+ action: 'updated',
151
+ revision: 2,
152
+ summary: 'Schedule updated: "History Schedule"',
153
+ changes: [{
154
+ field: 'status',
155
+ label: 'Status',
156
+ before: 'paused',
157
+ after: 'active',
158
+ }],
159
+ }],
160
+ createdAt: now,
161
+ updatedAt: now,
162
+ },
163
+ })
164
+
165
+ const response = await getScheduleHistory(
166
+ new Request('http://local/api/schedules/one/history', { method: 'GET' }),
167
+ routeParams('one'),
168
+ )
169
+
170
+ assert.equal(response.status, 200)
171
+ const payload = await response.json() as Record<string, unknown>
172
+ assert.equal(payload.scheduleId, 'one')
173
+ assert.equal(payload.revision, 2)
174
+ const history = payload.history as Array<Record<string, unknown>>
175
+ assert.equal(history.length, 1)
176
+ assert.equal(history[0].action, 'updated')
128
177
  })
@@ -0,0 +1,60 @@
1
+ import assert from 'node:assert/strict'
2
+ import test, { afterEach } from 'node:test'
3
+
4
+ import { GET as getScheduleHistory } from './[id]/history/route'
5
+ import { loadSchedules, saveSchedules } from '@/lib/server/storage'
6
+
7
+ const originalSchedules = loadSchedules()
8
+
9
+ function routeParams(id: string) {
10
+ return { params: Promise.resolve({ id }) }
11
+ }
12
+
13
+ afterEach(() => {
14
+ saveSchedules(originalSchedules)
15
+ })
16
+
17
+ test('GET /api/schedules/[id]/history returns normalized revision history', async () => {
18
+ const now = Date.now()
19
+ saveSchedules({
20
+ one: {
21
+ id: 'one',
22
+ name: 'History Schedule',
23
+ agentId: 'schedule-route-agent-history',
24
+ taskPrompt: 'Report changes',
25
+ scheduleType: 'interval',
26
+ intervalMs: 86_400_000,
27
+ status: 'active',
28
+ revision: 2,
29
+ history: [{
30
+ id: 'history-2',
31
+ at: now,
32
+ actor: 'user',
33
+ action: 'updated',
34
+ revision: 2,
35
+ summary: 'Schedule updated: "History Schedule"',
36
+ changes: [{
37
+ field: 'status',
38
+ label: 'Status',
39
+ before: 'paused',
40
+ after: 'active',
41
+ }],
42
+ }],
43
+ createdAt: now,
44
+ updatedAt: now,
45
+ },
46
+ })
47
+
48
+ const response = await getScheduleHistory(
49
+ new Request('http://local/api/schedules/one/history', { method: 'GET' }),
50
+ routeParams('one'),
51
+ )
52
+
53
+ assert.equal(response.status, 200)
54
+ const payload = await response.json() as Record<string, unknown>
55
+ assert.equal(payload.scheduleId, 'one')
56
+ assert.equal(payload.revision, 2)
57
+ const history = payload.history as Array<Record<string, unknown>>
58
+ assert.equal(history.length, 1)
59
+ assert.equal(history[0].action, 'updated')
60
+ })
package/src/cli/index.js CHANGED
@@ -210,6 +210,7 @@ const COMMAND_GROUPS = [
210
210
  description: 'Operator triage and readiness summaries',
211
211
  commands: [
212
212
  cmd('pulse', 'GET', '/operations/pulse', 'Get Operations Pulse summary (use --query range=24h or --query range=7d)'),
213
+ cmd('readiness', 'GET', '/quality/release-readiness', 'Get release readiness report (use --query agentId=... and --query suite=core for eval gate coverage)'),
213
214
  ],
214
215
  },
215
216
  {
@@ -578,6 +579,7 @@ const COMMAND_GROUPS = [
578
579
  commands: [
579
580
  cmd('list', 'GET', '/schedules', 'List schedules'),
580
581
  cmd('get', 'GET', '/schedules/:id', 'Get schedule by id'),
582
+ cmd('history', 'GET', '/schedules/:id/history', 'Get schedule revision history'),
581
583
  cmd('create', 'POST', '/schedules', 'Create schedule', { expectsJsonBody: true }),
582
584
  cmd('update', 'PUT', '/schedules/:id', 'Update schedule', { expectsJsonBody: true }),
583
585
  cmd('delete', 'DELETE', '/schedules/:id', 'Delete schedule'),
package/src/cli/index.ts CHANGED
@@ -608,6 +608,14 @@ export function buildProgram(): Command {
608
608
  await runWithHandler(this as Command, (ctx) => resolveByIdFromCollection(ctx, '/schedules', id))
609
609
  })
610
610
 
611
+ schedules
612
+ .command('history')
613
+ .description('Get schedule revision history')
614
+ .argument('<id>', 'Schedule id')
615
+ .action(async function (id: string) {
616
+ await runWithHandler(this as Command, (ctx) => apiRequest(ctx, 'GET', `/schedules/${encodeURIComponent(id)}/history`))
617
+ })
618
+
611
619
  schedules
612
620
  .command('create')
613
621
  .description('Create schedule')
package/src/cli/spec.js CHANGED
@@ -420,6 +420,7 @@ const COMMAND_GROUPS = {
420
420
  list: { description: 'List schedules', method: 'GET', path: '/schedules' },
421
421
  create: { description: 'Create schedule', method: 'POST', path: '/schedules' },
422
422
  get: { description: 'Get schedule by id (from list)', virtualGet: true, collectionPath: '/schedules', params: ['id'] },
423
+ history: { description: 'Get schedule revision history', method: 'GET', path: '/schedules/:id/history', params: ['id'] },
423
424
  update: { description: 'Update schedule', method: 'PUT', path: '/schedules/:id', params: ['id'] },
424
425
  delete: { description: 'Delete schedule', method: 'DELETE', path: '/schedules/:id', params: ['id'] },
425
426
  run: { description: 'Trigger schedule immediately', method: 'POST', path: '/schedules/:id/run', params: ['id'] },
@@ -15,6 +15,7 @@ import {
15
15
  summarizeEvalRuns,
16
16
  summarizeRunHealth,
17
17
  } from '@/lib/quality/quality-summary'
18
+ import type { ReleaseReadinessReport, ReleaseReadinessStatus } from '@/lib/quality/release-readiness'
18
19
  import { cn } from '@/lib/utils'
19
20
  import { useAppStore } from '@/stores/use-app-store'
20
21
  import type { EvalEnvironmentPlan, EvalGateResult, EvalRun, EvalSuiteResult } from '@/lib/server/eval/types'
@@ -129,6 +130,130 @@ function gateCheckClass(status: EvalGateResult['status']): string {
129
130
  return 'border-emerald-500/20 bg-emerald-500/[0.05] text-emerald-200'
130
131
  }
131
132
 
133
+ function readinessStatusClass(status: ReleaseReadinessStatus): string {
134
+ if (status === 'ready') return 'border-emerald-500/25 bg-emerald-500/10 text-emerald-200'
135
+ if (status === 'warning') return 'border-amber-500/25 bg-amber-500/10 text-amber-200'
136
+ return 'border-rose-500/25 bg-rose-500/10 text-rose-200'
137
+ }
138
+
139
+ function readinessScoreTone(status: ReleaseReadinessStatus): string {
140
+ if (status === 'ready') return 'text-emerald-300'
141
+ if (status === 'warning') return 'text-amber-300'
142
+ return 'text-rose-300'
143
+ }
144
+
145
+ function ReleaseReadinessPanel({
146
+ report,
147
+ loading,
148
+ onRefresh,
149
+ onOpenHref,
150
+ }: {
151
+ report: ReleaseReadinessReport | null
152
+ loading: boolean
153
+ onRefresh: () => void
154
+ onOpenHref: (href: string) => void
155
+ }) {
156
+ return (
157
+ <section className="rounded-[16px] border border-white/[0.06] bg-white/[0.025] p-4">
158
+ <div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
159
+ <div>
160
+ <div className="text-[11px] font-700 uppercase tracking-[0.12em] text-accent-bright/70">Release Readiness</div>
161
+ <h2 className="mt-1 font-display text-[17px] font-700 text-text">Ship gate report</h2>
162
+ <p className="mt-1 max-w-[680px] text-[12px] leading-relaxed text-text-3/65">
163
+ Combines eval regression gates, operations pulse blockers, pending approvals, active runs, budgets, connectors, and gateway readiness.
164
+ </p>
165
+ </div>
166
+ <button
167
+ type="button"
168
+ onClick={onRefresh}
169
+ disabled={loading}
170
+ className="shrink-0 rounded-[10px] border border-white/[0.08] bg-white/[0.04] px-3 py-2 text-[12px] font-800 text-text-2 transition-colors hover:bg-white/[0.08] disabled:opacity-40"
171
+ >
172
+ {loading ? 'Checking' : 'Refresh gate'}
173
+ </button>
174
+ </div>
175
+
176
+ {!report ? (
177
+ <div className="mt-4 rounded-[12px] border border-dashed border-white/[0.08] bg-white/[0.02] px-4 py-5 text-[12px] text-text-3/65">
178
+ {loading ? 'Building release readiness report...' : 'No release readiness report is available yet.'}
179
+ </div>
180
+ ) : (
181
+ <div className="mt-4 grid gap-4 xl:grid-cols-[260px_1fr]">
182
+ <div className="rounded-[14px] border border-white/[0.06] bg-white/[0.025] p-4">
183
+ <span className={cn('inline-flex rounded-full border px-2.5 py-1 text-[10px] font-800 uppercase tracking-[0.1em]', readinessStatusClass(report.status))}>
184
+ {report.status}
185
+ </span>
186
+ <div className={cn('mt-4 font-display text-[42px] font-700 tracking-[-0.04em]', readinessScoreTone(report.status))}>{report.score}</div>
187
+ <div className="mt-1 text-[12px] text-text-3/65">readiness score</div>
188
+ <div className="mt-4 grid grid-cols-2 gap-2">
189
+ <div className="rounded-[10px] bg-white/[0.035] px-3 py-2">
190
+ <div className="text-[10px] font-700 uppercase tracking-[0.1em] text-text-3/50">Blockers</div>
191
+ <div className="mt-1 text-[18px] font-800 text-text">{report.blockerCount}</div>
192
+ </div>
193
+ <div className="rounded-[10px] bg-white/[0.035] px-3 py-2">
194
+ <div className="text-[10px] font-700 uppercase tracking-[0.1em] text-text-3/50">Warnings</div>
195
+ <div className="mt-1 text-[18px] font-800 text-text">{report.warningCount}</div>
196
+ </div>
197
+ </div>
198
+ </div>
199
+
200
+ <div className="grid gap-3 lg:grid-cols-2">
201
+ <div className="rounded-[14px] border border-white/[0.06] bg-white/[0.02] p-3">
202
+ <div className="text-[12px] font-800 text-text">Checks</div>
203
+ <div className="mt-3 flex flex-col gap-2">
204
+ {report.checks.slice(0, 6).map((check) => (
205
+ <button
206
+ key={check.code}
207
+ type="button"
208
+ onClick={() => check.href && onOpenHref(check.href)}
209
+ className={cn(
210
+ 'rounded-[10px] border px-3 py-2 text-left transition-colors',
211
+ readinessStatusClass(check.status),
212
+ check.href ? 'hover:bg-white/[0.08]' : '',
213
+ )}
214
+ >
215
+ <div className="text-[11px] font-800 uppercase tracking-[0.08em]">{check.status}</div>
216
+ <div className="mt-1 text-[12px] font-700 text-text">{check.title}</div>
217
+ <div className="mt-0.5 text-[11px] leading-relaxed text-text-3/70">{check.summary}</div>
218
+ </button>
219
+ ))}
220
+ </div>
221
+ </div>
222
+
223
+ <div className="rounded-[14px] border border-white/[0.06] bg-white/[0.02] p-3">
224
+ <div className="text-[12px] font-800 text-text">Next actions</div>
225
+ <div className="mt-3 flex flex-col gap-2">
226
+ {report.nextActions.length === 0 ? (
227
+ <div className="rounded-[10px] border border-white/[0.06] bg-white/[0.025] px-3 py-4 text-[12px] text-text-3/65">
228
+ No triage actions are open in the selected window.
229
+ </div>
230
+ ) : (
231
+ report.nextActions.slice(0, 5).map((action) => (
232
+ <button
233
+ key={action.id}
234
+ type="button"
235
+ onClick={() => onOpenHref(action.href)}
236
+ className="rounded-[10px] border border-white/[0.06] bg-white/[0.025] px-3 py-2 text-left transition-colors hover:bg-white/[0.06]"
237
+ >
238
+ <div className="flex items-center justify-between gap-2">
239
+ <div className="text-[12px] font-800 text-text">{action.title}</div>
240
+ <span className={cn('rounded-full border px-2 py-0.5 text-[9px] font-800 uppercase tracking-[0.08em]', action.severity === 'high' ? 'border-rose-500/25 text-rose-200' : action.severity === 'medium' ? 'border-amber-500/25 text-amber-200' : 'border-emerald-500/25 text-emerald-200')}>
241
+ {action.severity}
242
+ </span>
243
+ </div>
244
+ <div className="mt-1 line-clamp-2 text-[11px] leading-relaxed text-text-3/65">{action.summary}</div>
245
+ </button>
246
+ ))
247
+ )}
248
+ </div>
249
+ </div>
250
+ </div>
251
+ </div>
252
+ )}
253
+ </section>
254
+ )
255
+ }
256
+
132
257
  function EvalEnvironmentPanel({ plan, loading, onRefresh }: {
133
258
  plan: EvalEnvironmentPlan | null
134
259
  loading: boolean
@@ -344,6 +469,8 @@ export function QualityWorkspace() {
344
469
  const [evalGateScope, setEvalGateScope] = useState<'scenario' | 'suite'>('scenario')
345
470
  const [evalGateLoading, setEvalGateLoading] = useState(false)
346
471
  const [evalBaselineBusy, setEvalBaselineBusy] = useState(false)
472
+ const [releaseReadiness, setReleaseReadiness] = useState<ReleaseReadinessReport | null>(null)
473
+ const [releaseReadinessLoading, setReleaseReadinessLoading] = useState(false)
347
474
  const [approvalBusy, setApprovalBusy] = useState<string | null>(null)
348
475
 
349
476
  useEffect(() => {
@@ -432,6 +559,25 @@ export function QualityWorkspace() {
432
559
  }
433
560
  }, [evalGateScope, selectedAgentId, selectedScenarioId, selectedSuite])
434
561
 
562
+ const loadReleaseReadiness = useCallback(async () => {
563
+ const params = new URLSearchParams({ range: '7d' })
564
+ if (selectedAgentId) {
565
+ params.set('agentId', selectedAgentId)
566
+ if (evalGateScope === 'scenario' && selectedScenarioId) params.set('scenarioId', selectedScenarioId)
567
+ if (evalGateScope === 'suite') params.set('suite', selectedSuite)
568
+ }
569
+ setReleaseReadinessLoading(true)
570
+ try {
571
+ const report = await api<ReleaseReadinessReport>('GET', `/quality/release-readiness?${params.toString()}`)
572
+ setReleaseReadiness(report)
573
+ } catch (err) {
574
+ setReleaseReadiness(null)
575
+ toast.error(err instanceof Error ? err.message : 'Unable to check release readiness')
576
+ } finally {
577
+ setReleaseReadinessLoading(false)
578
+ }
579
+ }, [evalGateScope, selectedAgentId, selectedScenarioId, selectedSuite])
580
+
435
581
  useEffect(() => {
436
582
  void loadQualityData()
437
583
  }, [loadQualityData])
@@ -454,6 +600,10 @@ export function QualityWorkspace() {
454
600
  void loadEvalGate()
455
601
  }, [loadEvalGate])
456
602
 
603
+ useEffect(() => {
604
+ void loadReleaseReadiness()
605
+ }, [loadReleaseReadiness])
606
+
457
607
  useEffect(() => {
458
608
  if (!suites.some((suite) => suite.name === selectedSuite) && suites[0]) {
459
609
  setSelectedSuite(suites[0].name)
@@ -495,12 +645,13 @@ export function QualityWorkspace() {
495
645
  await loadQualityData({ silent: true })
496
646
  await loadEvalEnvironmentPlan()
497
647
  await loadEvalGate()
648
+ await loadReleaseReadiness()
498
649
  } catch (err) {
499
650
  toast.error(err instanceof Error ? err.message : 'Eval scenario failed')
500
651
  } finally {
501
652
  setEvalBusy(null)
502
653
  }
503
- }, [evalEnvironmentPlan, loadEvalEnvironmentPlan, loadEvalGate, loadQualityData, selectedAgentId, selectedScenarioId])
654
+ }, [evalEnvironmentPlan, loadEvalEnvironmentPlan, loadEvalGate, loadQualityData, loadReleaseReadiness, selectedAgentId, selectedScenarioId])
504
655
 
505
656
  const runSuite = useCallback(async (suiteName: string) => {
506
657
  if (!selectedAgentId) {
@@ -524,12 +675,13 @@ export function QualityWorkspace() {
524
675
  await loadQualityData({ silent: true })
525
676
  await loadEvalEnvironmentPlan()
526
677
  await loadEvalGate()
678
+ await loadReleaseReadiness()
527
679
  } catch (err) {
528
680
  toast.error(err instanceof Error ? err.message : 'Eval suite failed')
529
681
  } finally {
530
682
  setEvalBusy(null)
531
683
  }
532
- }, [evalEnvironmentPlan, loadEvalEnvironmentPlan, loadEvalGate, loadQualityData, selectedAgentId])
684
+ }, [evalEnvironmentPlan, loadEvalEnvironmentPlan, loadEvalGate, loadQualityData, loadReleaseReadiness, selectedAgentId])
533
685
 
534
686
  const setEvalBaseline = useCallback(async () => {
535
687
  if (!selectedAgentId) {
@@ -547,13 +699,14 @@ export function QualityWorkspace() {
547
699
  : { agentId: selectedAgentId, suite: selectedSuite, minPercent: evalGate?.minPercent ?? 80, maxRegressionPoints: evalGate?.maxRegressionPoints ?? 5 }
548
700
  const result = await api<{ gate: EvalGateResult }>('POST', '/eval/baselines', body)
549
701
  setEvalGate(result.gate)
702
+ await loadReleaseReadiness()
550
703
  toast.success('Eval baseline saved')
551
704
  } catch (err) {
552
705
  toast.error(err instanceof Error ? err.message : 'Unable to save eval baseline')
553
706
  } finally {
554
707
  setEvalBaselineBusy(false)
555
708
  }
556
- }, [evalGate, evalGateScope, selectedAgentId, selectedScenarioId, selectedSuite])
709
+ }, [evalGate, evalGateScope, loadReleaseReadiness, selectedAgentId, selectedScenarioId, selectedSuite])
557
710
 
558
711
  const actOnApproval = useCallback(async (approval: ApprovalRequest, approved: boolean) => {
559
712
  setApprovalBusy(approval.id)
@@ -561,12 +714,13 @@ export function QualityWorkspace() {
561
714
  await api('POST', '/approvals', { id: approval.id, approved })
562
715
  toast.success(approved ? 'Approval granted' : 'Approval denied')
563
716
  await loadQualityData({ silent: true })
717
+ await loadReleaseReadiness()
564
718
  } catch (err) {
565
719
  toast.error(err instanceof Error ? err.message : 'Unable to update approval')
566
720
  } finally {
567
721
  setApprovalBusy(null)
568
722
  }
569
- }, [loadQualityData])
723
+ }, [loadQualityData, loadReleaseReadiness])
570
724
 
571
725
  if (loading) {
572
726
  return (
@@ -630,6 +784,12 @@ export function QualityWorkspace() {
630
784
  {activeTab === 'overview' && (
631
785
  <div className="flex flex-col gap-6">
632
786
  <OperationsPulsePanel defaultRange="7d" compact />
787
+ <ReleaseReadinessPanel
788
+ report={releaseReadiness}
789
+ loading={releaseReadinessLoading}
790
+ onRefresh={() => void loadReleaseReadiness()}
791
+ onOpenHref={(href) => router.push(href)}
792
+ />
633
793
 
634
794
  <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
635
795
  <StatTile