@swarmclawai/swarmclaw 1.9.11 → 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.
@@ -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
  }
@@ -71,6 +71,7 @@ export async function getPlatform(platform: string) {
71
71
  case 'googlechat': return (await import('./googlechat')).default
72
72
  case 'matrix': return (await import('./matrix')).default
73
73
  case 'email': return (await import('./email')).default
74
+ case 'filequeue': return (await import('./filequeue')).default
74
75
  case 'swarmdock': return (await import('./swarmdock')).default
75
76
  }
76
77
 
@@ -181,7 +182,7 @@ async function _startConnectorImpl(connectorId: string): Promise<void> {
181
182
  botToken = swarmdockFallbackPrivateKey
182
183
  }
183
184
 
184
- if (!botToken && connector.platform !== 'whatsapp' && connector.platform !== 'openclaw' && connector.platform !== 'signal' && connector.platform !== 'email' && connector.platform !== 'swarmdock') {
185
+ if (!botToken && connector.platform !== 'whatsapp' && connector.platform !== 'openclaw' && connector.platform !== 'signal' && connector.platform !== 'email' && connector.platform !== 'filequeue' && connector.platform !== 'swarmdock') {
185
186
  throw new Error('No bot token configured')
186
187
  }
187
188
 
@@ -14,7 +14,7 @@ import { resolveImagePath } from '../resolve-image'
14
14
  // 1. Connector module resolution (getPlatform)
15
15
  // ---------------------------------------------------------------------------
16
16
  describe('getPlatform — connector module resolution', () => {
17
- const newPlatforms = ['matrix', 'googlechat', 'teams', 'signal', 'bluebubbles'] as const
17
+ const newPlatforms = ['matrix', 'googlechat', 'teams', 'signal', 'bluebubbles', 'filequeue'] as const
18
18
 
19
19
  for (const name of newPlatforms) {
20
20
  it(`returns a valid module for "${name}"`, async () => {
@@ -192,6 +192,7 @@ export async function autoStartConnectorIfNeeded(connector: Connector, body: Rec
192
192
  const hasCredentials = connector.platform === 'whatsapp'
193
193
  || connector.platform === 'openclaw'
194
194
  || (connector.platform === 'bluebubbles' && (!!connector.credentialId || !!connector.config.password))
195
+ || connector.platform === 'filequeue'
195
196
  || !!connector.credentialId
196
197
  if (!hasCredentials || body.autoStart === false) return
197
198
  try {
@@ -14,6 +14,7 @@ describe('connectorSupportsBinaryMedia — email', () => {
14
14
  it('still returns false for platforms that do not support outbound binary', () => {
15
15
  assert.equal(connectorSupportsBinaryMedia('signal'), false)
16
16
  assert.equal(connectorSupportsBinaryMedia('matrix'), false)
17
+ assert.equal(connectorSupportsBinaryMedia('filequeue'), false)
17
18
  })
18
19
  })
19
20