@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.
- package/README.md +19 -1
- package/package.json +3 -2
- package/src/app/api/quality/architecture-health/route.ts +16 -0
- package/src/app/api/quality/release-readiness/route.ts +6 -1
- package/src/app/home/page.tsx +1 -1
- package/src/cli/index.js +1 -0
- package/src/cli/index.ts +1 -1
- package/src/components/connectors/connector-sheet.tsx +36 -5
- package/src/components/quality/quality-workspace.tsx +155 -1
- package/src/components/shared/command-palette.tsx +1 -1
- package/src/components/shared/connector-platform-icon.test.ts +4 -0
- package/src/components/shared/connector-platform-icon.tsx +1 -0
- package/src/lib/connectors/connector-readiness.ts +17 -7
- package/src/lib/quality/architecture-health.test.ts +79 -0
- package/src/lib/quality/architecture-health.ts +451 -0
- package/src/lib/quality/release-readiness.test.ts +13 -0
- package/src/lib/quality/release-readiness.ts +36 -0
- package/src/lib/server/connectors/connector-lifecycle.ts +2 -1
- package/src/lib/server/connectors/connector-routing.test.ts +1 -1
- package/src/lib/server/connectors/connector-service.ts +1 -0
- package/src/lib/server/connectors/email.test.ts +1 -0
- package/src/lib/server/connectors/filequeue.test.ts +141 -0
- package/src/lib/server/connectors/filequeue.ts +324 -0
- package/src/lib/server/session-tools/crud.ts +1 -0
- package/src/lib/server/storage.ts +1 -1
- package/src/lib/validation/schemas.ts +1 -1
- package/src/types/connector.ts +1 -0
|
@@ -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
|
|