@swarmclawai/swarmclaw 1.9.12 → 1.9.13

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,15 @@ Operational docs: https://swarmclaw.ai/docs/observability
399
399
 
400
400
  ## Releases
401
401
 
402
+ ### v1.9.13 Highlights
403
+
404
+ Architecture health release: SwarmClaw now turns runtime ownership, dispatch, memory, startup, and quality evidence into a scored operator report.
405
+
406
+ - **Architecture Health report.** `/api/quality/architecture-health` returns a structured inventory of runtime domains, surfaces, owners, guardrails, tests, score, risks, warnings, and next actions.
407
+ - **Quality Center visibility.** `/quality` now shows a Runtime Ownership Map beside release readiness so operators can inspect dispatch, memory, startup, and quality coverage before shipping.
408
+ - **Release gate integration.** Release readiness includes architecture health when scoring the ship gate report, blocking or warning when ownership evidence is incomplete.
409
+ - **CLI access.** `swarmclaw operations architecture-health` exposes the same report for automation and release scripts.
410
+
402
411
  ### v1.9.12 Highlights
403
412
 
404
413
  Local file-queue connector release: operators can bridge SwarmClaw to filesystem inbox, outbox, archive, and error folders without a hosted message bus.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.9.12",
3
+ "version": "1.9.13",
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",
@@ -88,7 +88,7 @@
88
88
  "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",
89
89
  "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",
90
90
  "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",
91
- "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-execution-policy.test.ts src/lib/server/tasks/task-handoff.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
+ "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/quality/architecture-health.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-execution-policy.test.ts src/lib/server/tasks/task-handoff.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",
92
92
  "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",
93
93
  "test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
94
94
  "test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
@@ -0,0 +1,16 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { buildArchitectureHealthReport } from '@/lib/quality/architecture-health'
3
+ import { errorMessage } from '@/lib/shared-utils'
4
+
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ export async function GET() {
8
+ try {
9
+ return NextResponse.json(buildArchitectureHealthReport())
10
+ } catch (err: unknown) {
11
+ return NextResponse.json(
12
+ { error: errorMessage(err) },
13
+ { status: 500 },
14
+ )
15
+ }
16
+ }
@@ -1,6 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { evaluateEvalGate } from '@/lib/server/eval/baseline'
3
3
  import { getOperationPulse, normalizeOperationPulseRange } from '@/lib/server/operations/operation-pulse'
4
+ import { buildArchitectureHealthReport } from '@/lib/quality/architecture-health'
4
5
  import { buildReleaseReadinessReport } from '@/lib/quality/release-readiness'
5
6
  import { errorMessage } from '@/lib/shared-utils'
6
7
 
@@ -28,7 +29,11 @@ export async function GET(req: Request) {
28
29
  })
29
30
  : null
30
31
 
31
- return NextResponse.json(buildReleaseReadinessReport({ pulse, evalGate }))
32
+ return NextResponse.json(buildReleaseReadinessReport({
33
+ pulse,
34
+ evalGate,
35
+ architectureHealth: buildArchitectureHealthReport(),
36
+ }))
32
37
  } catch (err: unknown) {
33
38
  return NextResponse.json(
34
39
  { error: errorMessage(err) },
package/src/cli/index.js CHANGED
@@ -211,6 +211,7 @@ const COMMAND_GROUPS = [
211
211
  commands: [
212
212
  cmd('pulse', 'GET', '/operations/pulse', 'Get Operations Pulse summary (use --query range=24h or --query range=7d)'),
213
213
  cmd('readiness', 'GET', '/quality/release-readiness', 'Get release readiness report (use --query agentId=... and --query suite=core for eval gate coverage)'),
214
+ cmd('architecture-health', 'GET', '/quality/architecture-health', 'Get architecture health inventory and drift report'),
214
215
  ],
215
216
  },
216
217
  {
@@ -15,6 +15,7 @@ import {
15
15
  summarizeEvalRuns,
16
16
  summarizeRunHealth,
17
17
  } from '@/lib/quality/quality-summary'
18
+ import type { ArchitectureHealthReport, ArchitectureHealthStatus } from '@/lib/quality/architecture-health'
18
19
  import type { ReleaseReadinessReport, ReleaseReadinessStatus } from '@/lib/quality/release-readiness'
19
20
  import { cn } from '@/lib/utils'
20
21
  import { useAppStore } from '@/stores/use-app-store'
@@ -142,6 +143,18 @@ function readinessScoreTone(status: ReleaseReadinessStatus): string {
142
143
  return 'text-rose-300'
143
144
  }
144
145
 
146
+ function architectureStatusClass(status: ArchitectureHealthStatus): string {
147
+ if (status === 'healthy') return 'border-emerald-500/25 bg-emerald-500/10 text-emerald-200'
148
+ if (status === 'watch') return 'border-amber-500/25 bg-amber-500/10 text-amber-200'
149
+ return 'border-rose-500/25 bg-rose-500/10 text-rose-200'
150
+ }
151
+
152
+ function architectureScoreTone(status: ArchitectureHealthStatus): string {
153
+ if (status === 'healthy') return 'text-emerald-300'
154
+ if (status === 'watch') return 'text-amber-300'
155
+ return 'text-rose-300'
156
+ }
157
+
145
158
  function ReleaseReadinessPanel({
146
159
  report,
147
160
  loading,
@@ -254,6 +267,118 @@ function ReleaseReadinessPanel({
254
267
  )
255
268
  }
256
269
 
270
+ function ArchitectureHealthPanel({
271
+ report,
272
+ loading,
273
+ onRefresh,
274
+ onOpenHref,
275
+ }: {
276
+ report: ArchitectureHealthReport | null
277
+ loading: boolean
278
+ onRefresh: () => void
279
+ onOpenHref: (href: string) => void
280
+ }) {
281
+ return (
282
+ <section className="rounded-[16px] border border-white/[0.06] bg-white/[0.025] p-4">
283
+ <div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
284
+ <div>
285
+ <div className="text-[11px] font-700 uppercase tracking-[0.12em] text-accent-bright/70">Architecture Health</div>
286
+ <h2 className="mt-1 font-display text-[17px] font-700 text-text">Runtime ownership map</h2>
287
+ <p className="mt-1 max-w-[680px] text-[12px] leading-relaxed text-text-3/65">
288
+ Inventories dispatch, memory, startup, and quality surfaces with owners, guardrails, and test evidence.
289
+ </p>
290
+ </div>
291
+ <button
292
+ type="button"
293
+ onClick={onRefresh}
294
+ disabled={loading}
295
+ 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"
296
+ >
297
+ {loading ? 'Checking' : 'Refresh map'}
298
+ </button>
299
+ </div>
300
+
301
+ {!report ? (
302
+ <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">
303
+ {loading ? 'Building architecture health report...' : 'No architecture health report is available yet.'}
304
+ </div>
305
+ ) : (
306
+ <div className="mt-4 grid gap-4 xl:grid-cols-[260px_1fr]">
307
+ <div className="rounded-[14px] border border-white/[0.06] bg-white/[0.025] p-4">
308
+ <span className={cn('inline-flex rounded-full border px-2.5 py-1 text-[10px] font-800 uppercase tracking-[0.1em]', architectureStatusClass(report.status))}>
309
+ {report.status}
310
+ </span>
311
+ <div className={cn('mt-4 font-display text-[42px] font-700 tracking-[-0.04em]', architectureScoreTone(report.status))}>{report.score}</div>
312
+ <div className="mt-1 text-[12px] text-text-3/65">health score</div>
313
+ <div className="mt-4 grid grid-cols-2 gap-2">
314
+ <div className="rounded-[10px] bg-white/[0.035] px-3 py-2">
315
+ <div className="text-[10px] font-700 uppercase tracking-[0.1em] text-text-3/50">Surfaces</div>
316
+ <div className="mt-1 text-[18px] font-800 text-text">{report.surfaceCount}</div>
317
+ </div>
318
+ <div className="rounded-[10px] bg-white/[0.035] px-3 py-2">
319
+ <div className="text-[10px] font-700 uppercase tracking-[0.1em] text-text-3/50">Guardrails</div>
320
+ <div className="mt-1 text-[18px] font-800 text-text">{report.guardrailCount}</div>
321
+ </div>
322
+ </div>
323
+ </div>
324
+
325
+ <div className="grid gap-3 lg:grid-cols-2">
326
+ <div className="rounded-[14px] border border-white/[0.06] bg-white/[0.02] p-3">
327
+ <div className="text-[12px] font-800 text-text">Domains</div>
328
+ <div className="mt-3 grid gap-2">
329
+ {report.domains.map((domain) => (
330
+ <div key={domain.id} className="rounded-[10px] border border-white/[0.06] bg-white/[0.025] px-3 py-2">
331
+ <div className="flex items-center justify-between gap-2">
332
+ <div className="text-[12px] font-800 text-text">{domain.title}</div>
333
+ <span className={cn('rounded-full border px-2 py-0.5 text-[9px] font-800 uppercase tracking-[0.08em]', architectureStatusClass(domain.status))}>
334
+ {domain.status}
335
+ </span>
336
+ </div>
337
+ <div className="mt-1 text-[11px] leading-relaxed text-text-3/65">{domain.surfaces.length} surfaces, {domain.testPaths.length} evidence paths</div>
338
+ </div>
339
+ ))}
340
+ </div>
341
+ </div>
342
+
343
+ <div className="rounded-[14px] border border-white/[0.06] bg-white/[0.02] p-3">
344
+ <div className="text-[12px] font-800 text-text">Checks</div>
345
+ <div className="mt-3 flex flex-col gap-2">
346
+ {report.nextActions.length === 0 ? (
347
+ report.checks.filter((check) => check.status === 'healthy').slice(0, 4).map((check) => (
348
+ <button
349
+ key={check.code}
350
+ type="button"
351
+ onClick={() => check.href && onOpenHref(check.href)}
352
+ className="rounded-[10px] border border-emerald-500/20 bg-emerald-500/[0.05] px-3 py-2 text-left text-emerald-200 transition-colors hover:bg-emerald-500/[0.08]"
353
+ >
354
+ <div className="text-[11px] font-800 uppercase tracking-[0.08em]">{check.status}</div>
355
+ <div className="mt-1 text-[12px] font-700 text-text">{check.title}</div>
356
+ <div className="mt-0.5 text-[11px] leading-relaxed text-text-3/70">{check.summary}</div>
357
+ </button>
358
+ ))
359
+ ) : (
360
+ report.nextActions.slice(0, 5).map((action) => (
361
+ <button
362
+ key={action.id}
363
+ type="button"
364
+ onClick={() => onOpenHref(action.href)}
365
+ className={cn('rounded-[10px] border px-3 py-2 text-left transition-colors hover:bg-white/[0.08]', architectureStatusClass(action.severity))}
366
+ >
367
+ <div className="text-[11px] font-800 uppercase tracking-[0.08em]">{action.severity}</div>
368
+ <div className="mt-1 text-[12px] font-700 text-text">{action.title}</div>
369
+ <div className="mt-0.5 text-[11px] leading-relaxed text-text-3/70">{action.summary}</div>
370
+ </button>
371
+ ))
372
+ )}
373
+ </div>
374
+ </div>
375
+ </div>
376
+ </div>
377
+ )}
378
+ </section>
379
+ )
380
+ }
381
+
257
382
  function EvalEnvironmentPanel({ plan, loading, onRefresh }: {
258
383
  plan: EvalEnvironmentPlan | null
259
384
  loading: boolean
@@ -471,6 +596,8 @@ export function QualityWorkspace() {
471
596
  const [evalBaselineBusy, setEvalBaselineBusy] = useState(false)
472
597
  const [releaseReadiness, setReleaseReadiness] = useState<ReleaseReadinessReport | null>(null)
473
598
  const [releaseReadinessLoading, setReleaseReadinessLoading] = useState(false)
599
+ const [architectureHealth, setArchitectureHealth] = useState<ArchitectureHealthReport | null>(null)
600
+ const [architectureHealthLoading, setArchitectureHealthLoading] = useState(false)
474
601
  const [approvalBusy, setApprovalBusy] = useState<string | null>(null)
475
602
 
476
603
  useEffect(() => {
@@ -578,10 +705,27 @@ export function QualityWorkspace() {
578
705
  }
579
706
  }, [evalGateScope, selectedAgentId, selectedScenarioId, selectedSuite])
580
707
 
708
+ const loadArchitectureHealth = useCallback(async () => {
709
+ setArchitectureHealthLoading(true)
710
+ try {
711
+ const report = await api<ArchitectureHealthReport>('GET', '/quality/architecture-health')
712
+ setArchitectureHealth(report)
713
+ } catch (err) {
714
+ setArchitectureHealth(null)
715
+ toast.error(err instanceof Error ? err.message : 'Unable to check architecture health')
716
+ } finally {
717
+ setArchitectureHealthLoading(false)
718
+ }
719
+ }, [])
720
+
581
721
  useEffect(() => {
582
722
  void loadQualityData()
583
723
  }, [loadQualityData])
584
724
 
725
+ useEffect(() => {
726
+ void loadArchitectureHealth()
727
+ }, [loadArchitectureHealth])
728
+
585
729
  useWs('runs', () => { void loadQualityData({ silent: true }) }, 5000)
586
730
 
587
731
  useEffect(() => {
@@ -746,7 +890,11 @@ export function QualityWorkspace() {
746
890
  {refreshing && <span className="text-[11px] text-text-3/60">Refreshing...</span>}
747
891
  <button
748
892
  type="button"
749
- onClick={() => void loadQualityData({ silent: true })}
893
+ onClick={() => {
894
+ void loadQualityData({ silent: true })
895
+ void loadArchitectureHealth()
896
+ void loadReleaseReadiness()
897
+ }}
750
898
  className="inline-flex items-center gap-2 rounded-[10px] border border-white/[0.08] bg-white/[0.04] px-3 py-2 text-[12px] font-700 text-text-2 transition-colors hover:bg-white/[0.08]"
751
899
  >
752
900
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
@@ -790,6 +938,12 @@ export function QualityWorkspace() {
790
938
  onRefresh={() => void loadReleaseReadiness()}
791
939
  onOpenHref={(href) => router.push(href)}
792
940
  />
941
+ <ArchitectureHealthPanel
942
+ report={architectureHealth}
943
+ loading={architectureHealthLoading}
944
+ onRefresh={() => void loadArchitectureHealth()}
945
+ onOpenHref={(href) => router.push(href)}
946
+ />
793
947
 
794
948
  <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
795
949
  <StatTile
@@ -0,0 +1,79 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+
4
+ import {
5
+ buildArchitectureHealthReport,
6
+ DEFAULT_ARCHITECTURE_HEALTH_INVENTORY,
7
+ } from './architecture-health'
8
+
9
+ describe('architecture health report', () => {
10
+ it('summarizes the default runtime architecture inventory', () => {
11
+ const report = buildArchitectureHealthReport({
12
+ generatedAt: 100_000,
13
+ })
14
+
15
+ assert.equal(report.generatedAt, 100_000)
16
+ assert.equal(report.status, 'healthy')
17
+ assert.equal(report.score, 100)
18
+ assert.equal(report.domainCount, DEFAULT_ARCHITECTURE_HEALTH_INVENTORY.length)
19
+ assert.ok(report.surfaceCount >= 10)
20
+ assert.ok(report.guardrailCount >= 8)
21
+ assert.ok(report.checks.some((check) => check.code === 'dispatch_guardrail_coverage'))
22
+ assert.ok(report.checks.some((check) => check.code === 'memory_authority'))
23
+ assert.ok(report.checks.some((check) => check.code === 'startup_surface_inventory'))
24
+ })
25
+
26
+ it('warns when a domain has an unguarded surface', () => {
27
+ const report = buildArchitectureHealthReport({
28
+ generatedAt: 100_000,
29
+ inventory: [{
30
+ id: 'dispatch',
31
+ title: 'Dispatch',
32
+ summary: 'Test dispatch surface',
33
+ owner: 'runtime',
34
+ surfaces: [{
35
+ id: 'direct',
36
+ title: 'Direct run',
37
+ kind: 'dispatch',
38
+ path: 'src/lib/server/test.ts',
39
+ description: 'A dispatch path without a guardrail.',
40
+ guardrails: [],
41
+ evidence: ['No policy attached'],
42
+ }],
43
+ testPaths: ['src/lib/server/test.test.ts'],
44
+ }],
45
+ })
46
+
47
+ assert.equal(report.status, 'watch')
48
+ assert.ok(report.score < 100)
49
+ assert.equal(report.warningCount, 1)
50
+ assert.ok(report.checks.some((check) => check.code === 'dispatch_unguarded_surface'))
51
+ })
52
+
53
+ it('marks missing test coverage as an architecture risk', () => {
54
+ const report = buildArchitectureHealthReport({
55
+ generatedAt: 100_000,
56
+ inventory: [{
57
+ id: 'startup',
58
+ title: 'Startup',
59
+ summary: 'Startup entry points',
60
+ owner: 'runtime',
61
+ surfaces: [{
62
+ id: 'cli',
63
+ title: 'CLI',
64
+ kind: 'startup',
65
+ path: 'src/cli/index.js',
66
+ description: 'CLI startup surface.',
67
+ guardrails: ['route coverage'],
68
+ evidence: ['CLI starts the server'],
69
+ }],
70
+ testPaths: [],
71
+ }],
72
+ })
73
+
74
+ assert.equal(report.status, 'risk')
75
+ assert.ok(report.score <= 70)
76
+ assert.equal(report.riskCount, 1)
77
+ assert.ok(report.checks.some((check) => check.code === 'startup_missing_tests'))
78
+ })
79
+ })
@@ -0,0 +1,451 @@
1
+ export type ArchitectureHealthStatus = 'healthy' | 'watch' | 'risk'
2
+
3
+ export type ArchitectureSurfaceKind =
4
+ | 'dispatch'
5
+ | 'memory'
6
+ | 'startup'
7
+ | 'quality'
8
+
9
+ export interface ArchitectureHealthSurface {
10
+ id: string
11
+ title: string
12
+ kind: ArchitectureSurfaceKind
13
+ path: string
14
+ description: string
15
+ guardrails: string[]
16
+ evidence: string[]
17
+ }
18
+
19
+ export interface ArchitectureHealthDomainInput {
20
+ id: string
21
+ title: string
22
+ summary: string
23
+ owner: string
24
+ surfaces: ArchitectureHealthSurface[]
25
+ testPaths: string[]
26
+ }
27
+
28
+ export interface ArchitectureHealthCheck {
29
+ code: string
30
+ status: ArchitectureHealthStatus
31
+ title: string
32
+ summary: string
33
+ evidence?: string[]
34
+ href?: string
35
+ }
36
+
37
+ export interface ArchitectureHealthDomain extends ArchitectureHealthDomainInput {
38
+ status: ArchitectureHealthStatus
39
+ score: number
40
+ checkCodes: string[]
41
+ }
42
+
43
+ export interface ArchitectureHealthAction {
44
+ id: string
45
+ severity: Exclude<ArchitectureHealthStatus, 'healthy'>
46
+ title: string
47
+ summary: string
48
+ href: string
49
+ evidence: string[]
50
+ }
51
+
52
+ export interface ArchitectureHealthReport {
53
+ generatedAt: number
54
+ status: ArchitectureHealthStatus
55
+ score: number
56
+ domainCount: number
57
+ surfaceCount: number
58
+ guardrailCount: number
59
+ riskCount: number
60
+ warningCount: number
61
+ domains: ArchitectureHealthDomain[]
62
+ checks: ArchitectureHealthCheck[]
63
+ nextActions: ArchitectureHealthAction[]
64
+ }
65
+
66
+ const WATCH_PENALTY = 10
67
+ const RISK_PENALTY = 30
68
+
69
+ export const DEFAULT_ARCHITECTURE_HEALTH_INVENTORY: ArchitectureHealthDomainInput[] = [
70
+ {
71
+ id: 'dispatch',
72
+ title: 'Dispatch Boundaries',
73
+ summary: 'Agent, task, protocol, connector, and tool execution paths that can start model or tool work.',
74
+ owner: 'runtime',
75
+ surfaces: [
76
+ {
77
+ id: 'agent-loop',
78
+ title: 'Agent loop dispatch',
79
+ kind: 'dispatch',
80
+ path: 'src/lib/server/agents/main-agent-loop.ts',
81
+ description: 'Main chat and autonomous run loop for agent turns.',
82
+ guardrails: ['tool capability policy', 'approval hooks', 'mission budgets', 'structured internal payload stripping'],
83
+ evidence: ['WorkingStatePatchSchema', 'MessageClassificationSchema', 'ResponseCompletenessSchema'],
84
+ },
85
+ {
86
+ id: 'protocol-runs',
87
+ title: 'Protocol run dispatch',
88
+ kind: 'dispatch',
89
+ path: 'src/lib/server/protocols/protocol-service.ts',
90
+ description: 'Visual protocol runner, DAG lifecycle, and step processors.',
91
+ guardrails: ['DAG validation', 'run lifecycle repository', 'step output contracts'],
92
+ evidence: ['protocol-service.test.ts', 'protocol-normalization.test.ts', 'protocol-foreach.test.ts'],
93
+ },
94
+ {
95
+ id: 'task-execution',
96
+ title: 'Task execution dispatch',
97
+ kind: 'dispatch',
98
+ path: 'src/lib/server/tasks/task-service.ts',
99
+ description: 'Task creation, execution workspace, liveness, handoff, and quality gates.',
100
+ guardrails: ['task execution policy', 'task quality gate', 'handoff packet readiness checks'],
101
+ evidence: ['task-execution-policy.test.ts', 'task-validation.test.ts', 'task-handoff.test.ts'],
102
+ },
103
+ {
104
+ id: 'connector-ingress',
105
+ title: 'Connector ingress dispatch',
106
+ kind: 'dispatch',
107
+ path: 'src/lib/server/connectors/connector-service.ts',
108
+ description: 'Inbound connector messages routed into sessions or rooms.',
109
+ guardrails: ['connector schema validation', 'readiness checks', 'routing tests'],
110
+ evidence: ['connector-routing.test.ts', 'email.test.ts', 'filequeue.test.ts'],
111
+ },
112
+ {
113
+ id: 'session-tools',
114
+ title: 'Session tool dispatch',
115
+ kind: 'dispatch',
116
+ path: 'src/lib/server/session-tools.ts',
117
+ description: 'Tool registry, session tool execution, and managed tool surfaces.',
118
+ guardrails: ['zod tool schemas', 'capability router', 'approval matching'],
119
+ evidence: ['tool-capability-policy.test.ts', 'universal-tool-access.test.ts', 'manage-tasks.test.ts'],
120
+ },
121
+ ],
122
+ testPaths: [
123
+ 'src/lib/server/agents/agent-runtime-config.test.ts',
124
+ 'src/lib/server/protocols/protocol-service.test.ts',
125
+ 'src/lib/server/tasks/task-execution-policy.test.ts',
126
+ 'src/lib/server/connectors/connector-routing.test.ts',
127
+ 'src/lib/server/tool-capability-policy.test.ts',
128
+ ],
129
+ },
130
+ {
131
+ id: 'memory',
132
+ title: 'Memory Ownership',
133
+ summary: 'Authoritative working state, long-term memory, graph retrieval, and archive surfaces.',
134
+ owner: 'memory',
135
+ surfaces: [
136
+ {
137
+ id: 'working-state',
138
+ title: 'Working state service',
139
+ kind: 'memory',
140
+ path: 'src/lib/server/working-state/service.ts',
141
+ description: 'Structured short-term state and fact extraction for active sessions.',
142
+ guardrails: ['zod schemas', 'normalization', 'repository boundary'],
143
+ evidence: ['working-state/service.test.ts', 'working-state/extraction.ts'],
144
+ },
145
+ {
146
+ id: 'memory-policy',
147
+ title: 'Memory policy',
148
+ kind: 'memory',
149
+ path: 'src/lib/server/memory/memory-policy.ts',
150
+ description: 'Controls what can be written, retained, consolidated, and recalled.',
151
+ guardrails: ['policy tests', 'session memory scope', 'temporal decay'],
152
+ evidence: ['memory-policy.test.ts', 'session-memory-scope.test.ts'],
153
+ },
154
+ {
155
+ id: 'memory-graph',
156
+ title: 'Memory graph',
157
+ kind: 'memory',
158
+ path: 'src/lib/server/memory/memory-graph.ts',
159
+ description: 'Graph relationships and retrieval context for long-running work.',
160
+ guardrails: ['graph tests', 'memory retrieval tests', 'MMR ranking'],
161
+ evidence: ['memory-graph.test.ts', 'memory-retrieval.test.ts'],
162
+ },
163
+ {
164
+ id: 'session-archive',
165
+ title: 'Session archive memory',
166
+ kind: 'memory',
167
+ path: 'src/lib/server/memory/session-archive-memory.ts',
168
+ description: 'Archived session memory used after compaction or long-running autonomous work.',
169
+ guardrails: ['archive tests', 'freshness boundaries', 'session ownership'],
170
+ evidence: ['session-archive-memory.test.ts', 'memory-consolidation.test.ts'],
171
+ },
172
+ ],
173
+ testPaths: [
174
+ 'src/lib/server/working-state/service.test.ts',
175
+ 'src/lib/server/memory/memory-policy.test.ts',
176
+ 'src/lib/server/memory/memory-graph.test.ts',
177
+ 'src/lib/server/memory/session-archive-memory.test.ts',
178
+ ],
179
+ },
180
+ {
181
+ id: 'startup',
182
+ title: 'Startup Entry Points',
183
+ summary: 'CLI, web, desktop, daemon, and packaging paths that bootstrap the runtime.',
184
+ owner: 'platform',
185
+ surfaces: [
186
+ {
187
+ id: 'cli-server',
188
+ title: 'CLI server entry',
189
+ kind: 'startup',
190
+ path: 'src/cli/index.ts',
191
+ description: 'Package CLI, command routing, server start, and API command coverage.',
192
+ guardrails: ['API route coverage guard', 'binary router tests', 'pack dry run'],
193
+ evidence: ['src/cli/index.test.js', 'bin/swarmclaw.js'],
194
+ },
195
+ {
196
+ id: 'next-app',
197
+ title: 'Next app runtime',
198
+ kind: 'startup',
199
+ path: 'src/app',
200
+ description: 'Self-hosted web UI and API routes.',
201
+ guardrails: ['health route', 'browser smoke', 'type-check'],
202
+ evidence: ['healthz/route.test.ts', 'scripts/browser-e2e-smoke.ts'],
203
+ },
204
+ {
205
+ id: 'desktop-wrapper',
206
+ title: 'Desktop wrapper',
207
+ kind: 'startup',
208
+ path: 'electron/main.ts',
209
+ description: 'Electron wrapper around the standalone server with app-owned data directories.',
210
+ guardrails: ['local-only bind host', 'userData home root', 'native module rebuild smoke'],
211
+ evidence: ['scripts/build-electron.mjs', 'scripts/electron-after-pack.test.mjs'],
212
+ },
213
+ {
214
+ id: 'daemon',
215
+ title: 'Daemon lifecycle',
216
+ kind: 'startup',
217
+ path: 'src/app/api/daemon/route.ts',
218
+ description: 'Runtime daemon start, stop, health checks, and status paths.',
219
+ guardrails: ['daemon health check', 'safe action schema', 'CLI mapping'],
220
+ evidence: ['src/cli/index.test.js', 'src/app/api/daemon/health-check/route.ts'],
221
+ },
222
+ ],
223
+ testPaths: [
224
+ 'src/cli/index.test.js',
225
+ 'src/app/api/healthz/route.test.ts',
226
+ 'scripts/electron-after-pack.test.mjs',
227
+ 'scripts/browser-e2e-smoke.ts',
228
+ ],
229
+ },
230
+ {
231
+ id: 'quality',
232
+ title: 'Quality Evidence',
233
+ summary: 'Operator evidence surfaces that turn runtime state into release decisions.',
234
+ owner: 'quality',
235
+ surfaces: [
236
+ {
237
+ id: 'release-readiness',
238
+ title: 'Release readiness',
239
+ kind: 'quality',
240
+ path: 'src/lib/quality/release-readiness.ts',
241
+ description: 'Combines eval gates, operations pulse, approvals, budgets, and runtime readiness.',
242
+ guardrails: ['scored report', 'blocker and warning counts', 'next actions'],
243
+ evidence: ['release-readiness.test.ts', '/api/quality/release-readiness'],
244
+ },
245
+ {
246
+ id: 'operations-pulse',
247
+ title: 'Operations pulse',
248
+ kind: 'quality',
249
+ path: 'src/lib/server/operations/operation-pulse.ts',
250
+ description: 'Shared triage queue for failed runs, approvals, connectors, gateways, and budgets.',
251
+ guardrails: ['range normalization', 'severity ranking', 'operator hrefs'],
252
+ evidence: ['operation-pulse.test.ts', '/api/operations/pulse'],
253
+ },
254
+ {
255
+ id: 'eval-gates',
256
+ title: 'Eval regression gates',
257
+ kind: 'quality',
258
+ path: 'src/lib/server/eval/baseline.ts',
259
+ description: 'Compares latest eval evidence against thresholds and approved baselines.',
260
+ guardrails: ['baseline scope', 'regression thresholds', 'CLI commands'],
261
+ evidence: ['baseline.test.ts', '/api/eval/gate'],
262
+ },
263
+ ],
264
+ testPaths: [
265
+ 'src/lib/quality/release-readiness.test.ts',
266
+ 'src/lib/server/operations/operation-pulse.test.ts',
267
+ 'src/lib/server/eval/baseline.test.ts',
268
+ ],
269
+ },
270
+ ]
271
+
272
+ function worstStatus(statuses: ArchitectureHealthStatus[]): ArchitectureHealthStatus {
273
+ if (statuses.includes('risk')) return 'risk'
274
+ if (statuses.includes('watch')) return 'watch'
275
+ return 'healthy'
276
+ }
277
+
278
+ function statusPenalty(status: ArchitectureHealthStatus): number {
279
+ if (status === 'risk') return RISK_PENALTY
280
+ if (status === 'watch') return WATCH_PENALTY
281
+ return 0
282
+ }
283
+
284
+ function scoreFromChecks(checks: ArchitectureHealthCheck[]): number {
285
+ const penalty = checks.reduce((sum, check) => sum + statusPenalty(check.status), 0)
286
+ return Math.max(0, 100 - penalty)
287
+ }
288
+
289
+ function domainScore(checks: ArchitectureHealthCheck[]): number {
290
+ const actionable = checks.filter((check) => check.status !== 'healthy')
291
+ return scoreFromChecks(actionable)
292
+ }
293
+
294
+ function addCheck(checks: ArchitectureHealthCheck[], check: ArchitectureHealthCheck): void {
295
+ checks.push(check)
296
+ }
297
+
298
+ function plural(count: number, singular: string, pluralLabel = `${singular}s`): string {
299
+ return `${count} ${count === 1 ? singular : pluralLabel}`
300
+ }
301
+
302
+ function buildDomainChecks(domain: ArchitectureHealthDomainInput): ArchitectureHealthCheck[] {
303
+ const checks: ArchitectureHealthCheck[] = []
304
+
305
+ if (!domain.owner.trim()) {
306
+ addCheck(checks, {
307
+ code: `${domain.id}_missing_owner`,
308
+ status: 'risk',
309
+ title: `${domain.title} needs an owner`,
310
+ summary: 'This architecture domain does not declare an owner.',
311
+ evidence: [domain.id],
312
+ })
313
+ }
314
+
315
+ if (domain.surfaces.length === 0) {
316
+ addCheck(checks, {
317
+ code: `${domain.id}_missing_surfaces`,
318
+ status: 'risk',
319
+ title: `${domain.title} has no inventoried surfaces`,
320
+ summary: 'A quality report cannot reason about this domain until its runtime surfaces are listed.',
321
+ evidence: [domain.id],
322
+ })
323
+ }
324
+
325
+ if (domain.testPaths.length === 0) {
326
+ addCheck(checks, {
327
+ code: `${domain.id}_missing_tests`,
328
+ status: 'risk',
329
+ title: `${domain.title} has no mapped test evidence`,
330
+ summary: 'This domain needs at least one concrete test or verifier path before it can be treated as release-ready.',
331
+ evidence: [domain.id],
332
+ })
333
+ }
334
+
335
+ const unguarded = domain.surfaces.filter((surface) => surface.guardrails.length === 0)
336
+ if (unguarded.length > 0) {
337
+ addCheck(checks, {
338
+ code: `${domain.id}_unguarded_surface`,
339
+ status: 'watch',
340
+ title: `${domain.title} has unguarded surfaces`,
341
+ summary: `${plural(unguarded.length, 'surface')} missing explicit guardrails.`,
342
+ evidence: unguarded.map((surface) => `${surface.title}: ${surface.path}`),
343
+ })
344
+ }
345
+
346
+ return checks
347
+ }
348
+
349
+ function buildCrossDomainChecks(inventory: ArchitectureHealthDomainInput[]): ArchitectureHealthCheck[] {
350
+ const checks: ArchitectureHealthCheck[] = []
351
+ const surfaces = inventory.flatMap((domain) => domain.surfaces)
352
+ const dispatchSurfaces = surfaces.filter((surface) => surface.kind === 'dispatch')
353
+ const memoryDomain = inventory.find((domain) => domain.id === 'memory')
354
+ const startupDomain = inventory.find((domain) => domain.id === 'startup')
355
+
356
+ if (dispatchSurfaces.length > 0 && dispatchSurfaces.every((surface) => surface.guardrails.length > 0)) {
357
+ addCheck(checks, {
358
+ code: 'dispatch_guardrail_coverage',
359
+ status: 'healthy',
360
+ title: 'Dispatch surfaces declare guardrails',
361
+ summary: `${plural(dispatchSurfaces.length, 'dispatch surface')} mapped to policy, approval, schema, or lifecycle controls.`,
362
+ evidence: dispatchSurfaces.map((surface) => `${surface.title}: ${surface.guardrails.join(', ')}`),
363
+ href: '/quality',
364
+ })
365
+ }
366
+
367
+ if (memoryDomain && memoryDomain.surfaces.length >= 3 && memoryDomain.testPaths.length > 0) {
368
+ addCheck(checks, {
369
+ code: 'memory_authority',
370
+ status: 'healthy',
371
+ title: 'Memory ownership is explicit',
372
+ summary: 'Working state, policy, graph, and archive surfaces are inventoried with test evidence.',
373
+ evidence: memoryDomain.surfaces.map((surface) => surface.path),
374
+ href: '/memory',
375
+ })
376
+ }
377
+
378
+ if (startupDomain && startupDomain.surfaces.length >= 3 && startupDomain.testPaths.length > 0) {
379
+ addCheck(checks, {
380
+ code: 'startup_surface_inventory',
381
+ status: 'healthy',
382
+ title: 'Startup surfaces are inventoried',
383
+ summary: 'CLI, web, desktop, and daemon entry points are tracked with smoke or route evidence.',
384
+ evidence: startupDomain.surfaces.map((surface) => surface.path),
385
+ href: '/settings',
386
+ })
387
+ }
388
+
389
+ return checks
390
+ }
391
+
392
+ function makeAction(check: ArchitectureHealthCheck): ArchitectureHealthAction | null {
393
+ if (check.status === 'healthy') return null
394
+ return {
395
+ id: check.code,
396
+ severity: check.status,
397
+ title: check.title,
398
+ summary: check.summary,
399
+ href: check.href || '/quality',
400
+ evidence: check.evidence || [],
401
+ }
402
+ }
403
+
404
+ export function buildArchitectureHealthReport(input: {
405
+ generatedAt?: number
406
+ inventory?: ArchitectureHealthDomainInput[]
407
+ } = {}): ArchitectureHealthReport {
408
+ const generatedAt = input.generatedAt ?? Date.now()
409
+ const inventory = input.inventory ?? DEFAULT_ARCHITECTURE_HEALTH_INVENTORY
410
+ const domainChecks = new Map<string, ArchitectureHealthCheck[]>()
411
+ const checks: ArchitectureHealthCheck[] = []
412
+
413
+ for (const domain of inventory) {
414
+ const nextChecks = buildDomainChecks(domain)
415
+ domainChecks.set(domain.id, nextChecks)
416
+ checks.push(...nextChecks)
417
+ }
418
+
419
+ checks.push(...buildCrossDomainChecks(inventory))
420
+
421
+ const domains: ArchitectureHealthDomain[] = inventory.map((domain) => {
422
+ const actionableChecks = domainChecks.get(domain.id) ?? []
423
+ return {
424
+ ...domain,
425
+ status: worstStatus(actionableChecks.map((check) => check.status)),
426
+ score: domainScore(actionableChecks),
427
+ checkCodes: actionableChecks.map((check) => check.code),
428
+ }
429
+ })
430
+
431
+ const actionableChecks = checks.filter((check) => check.status !== 'healthy')
432
+ const reportStatus = worstStatus(actionableChecks.map((check) => check.status))
433
+ const warningCount = actionableChecks.filter((check) => check.status === 'watch').length
434
+ const riskCount = actionableChecks.filter((check) => check.status === 'risk').length
435
+
436
+ return {
437
+ generatedAt,
438
+ status: reportStatus,
439
+ score: scoreFromChecks(actionableChecks),
440
+ domainCount: inventory.length,
441
+ surfaceCount: inventory.reduce((sum, domain) => sum + domain.surfaces.length, 0),
442
+ guardrailCount: inventory.reduce((sum, domain) => (
443
+ sum + domain.surfaces.reduce((surfaceSum, surface) => surfaceSum + surface.guardrails.length, 0)
444
+ ), 0),
445
+ riskCount,
446
+ warningCount,
447
+ domains,
448
+ checks,
449
+ nextActions: actionableChecks.map(makeAction).filter((action): action is ArchitectureHealthAction => action !== null),
450
+ }
451
+ }
@@ -1,6 +1,7 @@
1
1
  import assert from 'node:assert/strict'
2
2
  import { describe, it } from 'node:test'
3
3
 
4
+ import { buildArchitectureHealthReport } from './architecture-health'
4
5
  import { buildReleaseReadinessReport } from './release-readiness'
5
6
  import type { EvalGateResult } from '@/lib/server/eval/types'
6
7
  import type { OperationPulse } from '@/types'
@@ -126,4 +127,16 @@ describe('release readiness report', () => {
126
127
  assert.ok(report.checks.some((check) => check.code === 'failed_runs_present'))
127
128
  assert.ok(report.checks.some((check) => check.code === 'pending_approvals_present'))
128
129
  })
130
+
131
+ it('includes architecture health when supplied', () => {
132
+ const report = buildReleaseReadinessReport({
133
+ pulse: pulse(),
134
+ evalGate: evalGate(),
135
+ architectureHealth: buildArchitectureHealthReport({ generatedAt: now }),
136
+ })
137
+
138
+ assert.equal(report.status, 'ready')
139
+ assert.equal(report.architectureHealth?.status, 'healthy')
140
+ assert.ok(report.checks.some((check) => check.code === 'architecture_health_passed'))
141
+ })
129
142
  })
@@ -1,4 +1,5 @@
1
1
  import type { EvalGateResult } from '@/lib/server/eval/types'
2
+ import type { ArchitectureHealthReport } from '@/lib/quality/architecture-health'
2
3
  import type { OperationPulse, OperationPulseAction, OperationPulseRange } from '@/types'
3
4
 
4
5
  export type ReleaseReadinessStatus = 'ready' | 'warning' | 'blocked'
@@ -21,6 +22,7 @@ export interface ReleaseReadinessReport {
21
22
  warningCount: number
22
23
  pulse: OperationPulse
23
24
  evalGate: EvalGateResult | null
25
+ architectureHealth: ArchitectureHealthReport | null
24
26
  checks: ReleaseReadinessCheck[]
25
27
  nextActions: OperationPulseAction[]
26
28
  }
@@ -54,9 +56,11 @@ function addCheck(checks: ReleaseReadinessCheck[], check: ReleaseReadinessCheck)
54
56
  export function buildReleaseReadinessReport(input: {
55
57
  pulse: OperationPulse
56
58
  evalGate?: EvalGateResult | null
59
+ architectureHealth?: ArchitectureHealthReport | null
57
60
  }): ReleaseReadinessReport {
58
61
  const checks: ReleaseReadinessCheck[] = []
59
62
  const evalGate = input.evalGate ?? null
63
+ const architectureHealth = input.architectureHealth ?? null
60
64
 
61
65
  if (!evalGate) {
62
66
  addCheck(checks, {
@@ -169,6 +173,37 @@ export function buildReleaseReadinessReport(input: {
169
173
  })
170
174
  }
171
175
 
176
+ if (architectureHealth) {
177
+ if (architectureHealth.status === 'risk') {
178
+ addCheck(checks, {
179
+ code: 'architecture_health_risk',
180
+ status: 'blocked',
181
+ title: 'Architecture health has risks',
182
+ summary: `${plural(architectureHealth.riskCount, 'architecture risk')} need review before release.`,
183
+ href: '/quality',
184
+ evidence: architectureHealth.nextActions.map((action) => action.summary),
185
+ })
186
+ } else if (architectureHealth.status === 'watch') {
187
+ addCheck(checks, {
188
+ code: 'architecture_health_watch',
189
+ status: 'warning',
190
+ title: 'Architecture health needs review',
191
+ summary: `${plural(architectureHealth.warningCount, 'architecture warning')} found in runtime ownership checks.`,
192
+ href: '/quality',
193
+ evidence: architectureHealth.nextActions.map((action) => action.summary),
194
+ })
195
+ } else {
196
+ addCheck(checks, {
197
+ code: 'architecture_health_passed',
198
+ status: 'ready',
199
+ title: 'Architecture health passed',
200
+ summary: 'Dispatch, memory, startup, and quality surfaces have mapped owners, guardrails, and test evidence.',
201
+ href: '/quality',
202
+ evidence: [`${architectureHealth.score} health score`],
203
+ })
204
+ }
205
+ }
206
+
172
207
  const blockerCount = checks.filter((check) => check.status === 'blocked').length
173
208
  const warningCount = checks.filter((check) => check.status === 'warning').length
174
209
 
@@ -181,6 +216,7 @@ export function buildReleaseReadinessReport(input: {
181
216
  warningCount,
182
217
  pulse: input.pulse,
183
218
  evalGate,
219
+ architectureHealth,
184
220
  checks,
185
221
  nextActions: input.pulse.actions.slice(0, 8),
186
222
  }